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 %} + - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - {% load static %} - - {% block head %}{% endblock %} + + + {% block title %}{{ site_settings.business_name }}{% endblock %} + + {% if project_description %} + + + {% endif %} + + + + + + {% if LANGUAGE_CODE == 'ar' %} + + {% else %} + + {% endif %} + + + + + {% block head %}{% endblock %} - - {% block content %}{% endblock %} - - +
+ + + + +
+ + +
+ {% block content %}{% endblock %} +
+
+
+ + + + {% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/core/templates/core/customers.html b/core/templates/core/customers.html new file mode 100644 index 0000000..5a0219e --- /dev/null +++ b/core/templates/core/customers.html @@ -0,0 +1,103 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Customers" %} | Meezan Accounting{% endblock %} + +{% block content %} +
+
+

{% trans "Customers" %}

+ +
+ + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + +
+
+
+ + + + + + + + + + + + + {% for customer in customers %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Name" %}{% trans "Phone" %}{% trans "Email" %}{% trans "Address" %}{% trans "Total Sales" %}{% trans "Actions" %}
{{ customer.name }}{{ customer.phone }}{{ customer.email }}{{ customer.address|truncatechars:30 }}{{ site_settings.currency_symbol }}{{ customer.total_sales|default:"0.00" }} + + +
+ {% trans "No customers found." %} +
+
+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..74ff132 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,220 @@ -{% extends "base.html" %} +{% extends 'base.html' %} +{% load static i18n %} -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% block title %}{% trans "Smart Dashboard" %} - {{ site_settings.business_name }}{% endblock %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+ +
+
+

{% trans "Overview" %}

+

{% trans "Welcome back! Here's what's happening with your business today." %}

+
+
-

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 "Total Revenue" %}
+

{{ site_settings.currency_symbol }}{{ total_sales_amount|floatformat:2 }}

+
+
+
+
+
+
+
+
+ +
+
+
{% trans "Total Sales" %}
+

{{ total_sales_count }}

+
+
+
+
+
+
+
+
+ +
+
+
{% trans "Total Products" %}
+

{{ total_products }}

+
+
+
+
+
+
+
+
+ +
+
+
{% trans "Total Customers" %}
+

{{ total_customers }}

+
+
+
+
+
+ +
+ +
+
+
+
{% trans "Sales Revenue" %}
+ {% trans "Last 7 Days" %} +
+ +
+
+ + +
+
+
{% trans "Low Stock Alerts" %}
+ {% if low_stock_products %} +
    + {% for product in low_stock_products %} +
  • +
    +
    + +
    +
    +

    + {% if LANGUAGE_CODE == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %} +

    + + {% if LANGUAGE_CODE == 'ar' %}{{ product.category.name_ar }}{% else %}{{ product.category.name_en }}{% endif %} + +
    +
    + {{ product.stock_quantity }} +
  • + {% endfor %} +
+ {% else %} +
+ +

{% trans "All stock levels are healthy!" %}

+
+ {% endif %} + +
+
+
+ + +
+
+
+
{% trans "Recent Sales" %}
+
+ + + + + + + + + + + + + {% for sale in recent_sales %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Sale ID" %}{% trans "Customer" %}{% trans "Date" %}{% trans "Total Amount" %}{% trans "Status" %}{% trans "Action" %}
#{{ sale.id }}{{ sale.customer.name|default:"Guest" }}{{ sale.created_at|date:"M d, Y H:i" }}{{ site_settings.currency_symbol }}{{ sale.total_amount }}{% trans "Completed" %} + +
{% trans "No recent sales found." %}
+
+
+
+
+ +{% endblock %} + +{% block scripts %} + + {% endblock %} \ No newline at end of file diff --git a/core/templates/core/inventory.html b/core/templates/core/inventory.html new file mode 100644 index 0000000..22398e6 --- /dev/null +++ b/core/templates/core/inventory.html @@ -0,0 +1,162 @@ +{% extends 'base.html' %} +{% load i18n static %} + +{% block title %}{% trans "Inventory Management" %} | Meezan Accounting{% endblock %} + +{% block content %} +
+
+
+

{% trans "Inventory Management" %}

+ +
+ +
+ + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + + +
+
+
+
+
+ + +
+
+
+ +
+
+ +
+
+
+
+ + +
+ {% for product in products %} +
+
+
+ {% if product.image %} + {{ product.name_en }} + {% else %} +
+ +
+ {% endif %} + + {% trans "In Stock" %}: {{ product.stock_quantity }} + +
+
+
+ {% if LANGUAGE_CODE == 'ar' %}{{ product.category.name_ar }}{% else %}{{ product.category.name_en }}{% endif %} +
+
+ {% if LANGUAGE_CODE == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %} +
+
+

{{ site_settings.currency_symbol }}{{ product.price }}

+
+ + +
+
+
+
+
+ {% empty %} +
+
+ +

{% trans "Your inventory is empty" %}

+

{% trans "Start by adding your first product to the system." %}

+ +
+
+ {% endfor %} +
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/pos.html b/core/templates/core/pos.html new file mode 100644 index 0000000..10c3e64 --- /dev/null +++ b/core/templates/core/pos.html @@ -0,0 +1,428 @@ +{% extends 'base.html' %} +{% load i18n static %} + +{% block title %}{% trans "POS" %} | {{ site_settings.business_name }}{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+
+ +
+
+

{% trans "Point of Sale" %}

+
+ + +
+
+ +
+
{% trans "All" %}
+ {% for category in categories %} +
+ {% if LANGUAGE_CODE == 'ar' %}{{ category.name_ar }}{% else %}{{ category.name_en }}{% endif %} +
+ {% endfor %} +
+ +
+ {% for product in products %} +
+
+ {% if product.image %} + {{ product.name_en }} + {% else %} +
+ +
+ {% endif %} +
+
+ {% if LANGUAGE_CODE == 'ar' %}{{ product.name_ar }}{% else %}{{ product.name_en }}{% endif %} +
+

{{ site_settings.currency_symbol }}{{ product.price }}

+ {% trans "Stock" %}: {{ product.stock_quantity }} +
+
+
+ {% endfor %} +
+
+ + +
+
+
+
{% trans "Current Order" %}
+ +
+ +
+
+ +
+
+ + {% trans "Your cart is empty" %} +
+
+ + +
+
+
+
+ + +
+
+
+
TAX INVOICEفاتورة ضريبية
+
+
+
+
+
+ Inv #: + رقم الفاتورة: +
+
+ Date: + التاريخ: +
+
+ + + + + + + + + + + +
Item / الصنفQtyTotal
+
+
+ TOTAL / المجموع + +
+
+
+ THANK YOU / شكراً لزيارتكم +
+
+ + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/core/templates/core/purchases.html b/core/templates/core/purchases.html new file mode 100644 index 0000000..603cdd3 --- /dev/null +++ b/core/templates/core/purchases.html @@ -0,0 +1,98 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Purchases" %} | Meezan Accounting{% endblock %} + +{% block content %} +
+
+

{% trans "Purchase History" %}

+ +
+ + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + +
+
+
+ + + + + + + + + + + {% for purchase in purchases %} + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Date" %}{% trans "Supplier" %}{% trans "Total Amount" %}{% trans "Actions" %}
{{ purchase.created_at|date:"Y-m-d H:i" }}{{ purchase.supplier.name|default:"-" }}{{ site_settings.currency_symbol }}{{ purchase.total_amount }} + +
+ {% trans "No purchases found." %} +
+
+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/reports.html b/core/templates/core/reports.html new file mode 100644 index 0000000..34f83b1 --- /dev/null +++ b/core/templates/core/reports.html @@ -0,0 +1,70 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Smart Reports" %} | Meezan Accounting{% endblock %} + +{% block content %} +
+
+
+

{% trans "Analytics & Reports" %}

+

{% trans "Deep dive into your business performance." %}

+
+
+ +
+ +
+
+
{% trans "Monthly Revenue" %}
+
+ + + + + + + + + {% for sale in monthly_sales %} + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Month" %}{% trans "Revenue" %}
{{ sale.month|date:"F Y" }}{{ site_settings.currency_symbol }}{{ sale.total|floatformat:2 }}
{% trans "No data available." %}
+
+
+
+ + +
+
+
{% trans "Top Selling Products" %}
+
    + {% for item in top_products %} +
  • +
    +

    + {% if LANGUAGE_CODE == 'ar' %}{{ item.product__name_ar }}{% else %}{{ item.product__name_en }}{% endif %} +

    + {{ item.total_qty }} {% trans "units sold" %} +
    +
    + {{ site_settings.currency_symbol }}{{ item.revenue|floatformat:2 }} +
    +
  • + {% empty %} +
  • {% trans "No sales data." %}
  • + {% endfor %} +
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/settings.html b/core/templates/core/settings.html new file mode 100644 index 0000000..7b85eb6 --- /dev/null +++ b/core/templates/core/settings.html @@ -0,0 +1,102 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block content %} +
+
+
+

{% trans "System Settings" %}

+

{% trans "Manage your business profile and preferences." %}

+
+
+ + {% if messages %} +
+
+ {% for message in messages %} + + {% endfor %} +
+
+ {% endif %} + +
+
+
+
+
{% trans "Business Profile" %}
+
+
+
+ {% csrf_token %} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
{% trans "Financial Preferences" %}
+
+ + +
{% trans "e.g., $, £, SAR, AED" %}
+
+
+ + +
+
+ +
+ +
+
+
+
+
+ +
+
+
+
{% trans "Help & Support" %}
+
+
+

+ {% trans "Need help configuring your smart admin? Check our documentation or contact support." %} +

+ + {% trans "Documentation" %} + +
+
+ +
+
+ +
{% trans "Smart Admin Version" %}
+

v2.1.0-Meezan

+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/suppliers.html b/core/templates/core/suppliers.html new file mode 100644 index 0000000..b0f699a --- /dev/null +++ b/core/templates/core/suppliers.html @@ -0,0 +1,95 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Suppliers" %} | Meezan Accounting{% endblock %} + +{% block content %} +
+
+

{% trans "Suppliers" %}

+ +
+ + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + +
+
+
+ + + + + + + + + + + {% for supplier in suppliers %} + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Name" %}{% trans "Contact Person" %}{% trans "Phone" %}{% trans "Actions" %}
{{ supplier.name }}{{ supplier.contact_person }}{{ supplier.phone }} + + +
+ {% trans "No suppliers found." %} +
+
+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 6299e3d..5fbb881 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,20 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), -] + path('', views.index, name='index'), + path('inventory/', views.inventory, name='inventory'), + path('pos/', views.pos, name='pos'), + path('customers/', views.customers, name='customers'), + path('suppliers/', views.suppliers, name='suppliers'), + path('purchases/', views.purchases, name='purchases'), + path('reports/', views.reports, name='reports'), + path('settings/', views.settings_view, name='settings'), + + # API / Actions + path('api/create-sale/', views.create_sale_api, name='create_sale_api'), + path('customers/add/', views.add_customer, name='add_customer'), + path('suppliers/add/', views.add_supplier, name='add_supplier'), + path('purchases/add/', views.add_purchase, name='add_purchase'), + path('inventory/add/', views.add_product, name='add_product'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c9aed12..78babbc 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,236 @@ -import os -import platform - -from django import get_version as django_version -from django.shortcuts import render +from django.shortcuts import render, get_object_or_404, redirect +from django.db.models import Sum, Count, F +from django.db.models.functions import TruncDate, TruncMonth +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +from .models import Product, Sale, Category, Customer, Supplier, Purchase, SaleItem, SystemSetting +import json +from datetime import timedelta from django.utils import timezone +from django.contrib import messages - -def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() +def index(request): + """ + Enhanced Meezan Dashboard View + """ + # Summary Stats + total_products = Product.objects.count() + total_sales_count = Sale.objects.count() + total_sales_amount = Sale.objects.aggregate(total=Sum('total_amount'))['total'] or 0 + total_customers = Customer.objects.count() + + # Stock Alert (Low stock < 5) + low_stock_products = Product.objects.filter(stock_quantity__lt=5) + + # Recent Transactions + recent_sales = Sale.objects.order_by('-created_at')[:5] + + # Chart Data: Sales for the last 7 days + seven_days_ago = timezone.now().date() - timedelta(days=6) + sales_over_time = Sale.objects.filter(created_at__date__gte=seven_days_ago) \ + .annotate(date=TruncDate('created_at')) \ + .values('date') \ + .annotate(total=Sum('total_amount')) \ + .order_by('date') + + # Prepare data for Chart.js + chart_labels = [] + chart_data = [] + + date_dict = {s['date']: float(s['total']) for s in sales_over_time} + for i in range(7): + date = seven_days_ago + timedelta(days=i) + chart_labels.append(date.strftime('%b %d')) + chart_data.append(date_dict.get(date, 0)) context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + 'total_products': total_products, + 'total_sales_count': total_sales_count, + 'total_sales_amount': total_sales_amount, + 'total_customers': total_customers, + 'low_stock_products': low_stock_products, + 'recent_sales': recent_sales, + 'chart_labels': json.dumps(chart_labels), + 'chart_data': json.dumps(chart_data), } - return render(request, "core/index.html", context) + return render(request, 'core/index.html', context) + +def inventory(request): + products = Product.objects.all().select_related('category') + categories = Category.objects.all() + context = {'products': products, 'categories': categories} + return render(request, 'core/inventory.html', context) + +def pos(request): + products = Product.objects.all().filter(stock_quantity__gt=0) + customers = Customer.objects.all() + categories = Category.objects.all() + context = {'products': products, 'customers': customers, 'categories': categories} + return render(request, 'core/pos.html', context) + +def customers(request): + customers_list = Customer.objects.all().annotate(total_sales=Sum('sale__total_amount')) + context = {'customers': customers_list} + return render(request, 'core/customers.html', context) + +def suppliers(request): + suppliers_list = Supplier.objects.all() + context = {'suppliers': suppliers_list} + return render(request, 'core/suppliers.html', context) + +def purchases(request): + purchases_list = Purchase.objects.all().select_related('supplier') + suppliers_list = Supplier.objects.all() + context = {'purchases': purchases_list, 'suppliers': suppliers_list} + return render(request, 'core/purchases.html', context) + +def reports(request): + """ + Smart Reports View + """ + # Monthly Revenue + monthly_sales = Sale.objects.annotate(month=TruncMonth('created_at')) \ + .values('month') \ + .annotate(total=Sum('total_amount')) \ + .order_by('-month')[:12] + + # Top Selling Products + top_products = SaleItem.objects.values('product__name_en', 'product__name_ar') \ + .annotate(total_qty=Sum('quantity'), revenue=Sum('line_total')) \ + .order_by('-total_qty')[:5] + + context = { + 'monthly_sales': monthly_sales, + 'top_products': top_products, + } + return render(request, 'core/reports.html', context) + +def settings_view(request): + """ + Smart Admin Settings View + """ + settings = SystemSetting.objects.first() + if not settings: + settings = SystemSetting.objects.create() + + if request.method == 'POST': + settings.business_name = request.POST.get('business_name') + settings.address = request.POST.get('address') + settings.phone = request.POST.get('phone') + settings.email = request.POST.get('email') + settings.currency_symbol = request.POST.get('currency_symbol') + settings.tax_rate = request.POST.get('tax_rate') + settings.save() + messages.success(request, "Settings updated successfully!") + return redirect('settings') + + return render(request, 'core/settings.html', {'settings': settings}) + +@csrf_exempt +def create_sale_api(request): + if request.method == 'POST': + try: + data = json.loads(request.body) + customer_id = data.get('customer_id') + items = data.get('items', []) + total_amount = data.get('total_amount', 0) + discount = data.get('discount', 0) + + customer = None + if customer_id: + customer = Customer.objects.get(id=customer_id) + + sale = Sale.objects.create( + customer=customer, + total_amount=total_amount, + discount=discount + ) + + for item in items: + product = Product.objects.get(id=item['id']) + SaleItem.objects.create( + sale=sale, + product=product, + quantity=item['quantity'], + unit_price=item['price'], + line_total=item['line_total'] + ) + product.stock_quantity -= item['quantity'] + product.save() + + settings = SystemSetting.objects.first() + return JsonResponse({ + 'success': True, + 'sale_id': sale.id, + 'business': { + 'name': settings.business_name, + 'address': settings.address, + 'phone': settings.phone, + 'currency': settings.currency_symbol + }, + 'sale': { + 'id': sale.id, + 'created_at': sale.created_at.strftime("%Y-%m-%d %H:%M"), + 'total': float(sale.total_amount), + 'items': [ + { + 'name_en': item.product.name_en, + 'name_ar': item.product.name_ar, + 'qty': item.quantity, + 'price': float(item.unit_price), + 'total': float(item.line_total) + } for item in sale.items.all() + ] + } + }) + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}, status=400) + return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) + +def add_customer(request): + if request.method == 'POST': + name = request.POST.get('name') + phone = request.POST.get('phone') + email = request.POST.get('email') + address = request.POST.get('address') + Customer.objects.create(name=name, phone=phone, email=email, address=address) + messages.success(request, "Customer added successfully!") + return redirect('customers') + +def add_supplier(request): + if request.method == 'POST': + name = request.POST.get('name') + contact_person = request.POST.get('contact_person') + phone = request.POST.get('phone') + Supplier.objects.create(name=name, contact_person=contact_person, phone=phone) + messages.success(request, "Supplier added successfully!") + return redirect('suppliers') + +def add_purchase(request): + if request.method == 'POST': + supplier_id = request.POST.get('supplier') + total_amount = request.POST.get('total_amount') + supplier = get_object_or_404(Supplier, id=supplier_id) + Purchase.objects.create(supplier=supplier, total_amount=total_amount) + messages.success(request, "Purchase recorded successfully!") + return redirect('purchases') + +def add_product(request): + if request.method == 'POST': + name_en = request.POST.get('name_en') + name_ar = request.POST.get('name_ar') + category_id = request.POST.get('category') + sku = request.POST.get('sku') + price = request.POST.get('price') + stock = request.POST.get('stock') + category = get_object_or_404(Category, id=category_id) + Product.objects.create( + name_en=name_en, + name_ar=name_ar, + category=category, + sku=sku, + price=price, + stock_quantity=stock + ) + messages.success(request, "Product added successfully!") + return redirect('inventory') diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..a3ebcdc 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,159 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +/* Meezan Accounting Custom Styles */ +:root { + --meezan-primary: #2E5BFF; + --meezan-secondary: #8C9EFF; + --meezan-accent: #FFAB40; + --meezan-bg: #F8FAFF; + --meezan-sidebar-bg: #FFFFFF; + --meezan-text: #1A202C; + --meezan-glass: rgba(255, 255, 255, 0.7); + --sidebar-width: 260px; +} + +body { + font-family: 'Plus Jakarta Sans', 'Cairo', sans-serif; + background-color: var(--meezan-bg); + color: var(--meezan-text); + overflow-x: hidden; +} + +/* RTL Specific Tweaks */ +[dir="rtl"] { + font-family: 'Cairo', 'Plus Jakarta Sans', sans-serif; +} + +.bg-primary { + background-color: var(--meezan-primary) !important; +} + +.text-primary { + color: var(--meezan-primary) !important; +} + +/* Glassmorphism Effect */ +.glass-card { + background: var(--meezan-glass); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 16px; + box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07); +} + +/* Sidebar Styling */ +#wrapper { + display: flex; + width: 100%; + align-items: stretch; +} + +#sidebar { + min-width: var(--sidebar-width); + max-width: var(--sidebar-width); + background: var(--meezan-sidebar-bg); + color: var(--meezan-text); + transition: all 0.3s; + border-inline-end: 1px solid rgba(0,0,0,0.05); + z-index: 1000; + position: sticky; + top: 0; + height: 100vh; +} + +#sidebar.active { + margin-inline-start: calc(-1 * var(--sidebar-width)); +} + +#sidebar .sidebar-header { + padding: 20px; + background: var(--meezan-sidebar-bg); +} + +#sidebar ul.components { + padding: 20px 0; +} + +#sidebar ul li a { + padding: 12px 25px; + font-size: 1rem; + display: flex; + align-items: center; + color: var(--meezan-text); + text-decoration: none; + transition: all 0.3s; + border-radius: 0 50px 50px 0; + margin-inline-end: 15px; +} + +[dir="rtl"] #sidebar ul li a { + border-radius: 50px 0 0 50px; + margin-inline-start: 15px; + margin-inline-end: 0; +} + +#sidebar ul li a:hover, #sidebar ul li a.active { + background: rgba(46, 91, 255, 0.1); + color: var(--meezan-primary); + font-weight: 600; +} + +#sidebar ul li a i { + margin-inline-end: 15px; + font-size: 1.2rem; +} + +/* Main Content Styling */ +#content { + width: 100%; + padding: 0; + min-height: 100vh; + transition: all 0.3s; +} + +.top-navbar { + padding: 15px 30px; + background: #fff; + border-bottom: 1px solid rgba(0,0,0,0.05); +} + +/* Hero Section */ +.hero-gradient { + background: linear-gradient(135deg, var(--meezan-primary) 0%, var(--meezan-secondary) 100%); + border-radius: 24px; + padding: 3rem; + color: white; + position: relative; + overflow: hidden; +} + +/* Buttons */ +.btn-primary { + background-color: var(--meezan-primary); + border: none; + padding: 0.6rem 1.5rem; + border-radius: 10px; + font-weight: 600; + transition: all 0.3s ease; +} + +/* Dashboard Stats */ +.stat-card { + transition: transform 0.3s ease; +} + +.stat-card:hover { + transform: translateY(-5px); +} + +/* Mobile Tweaks */ +@media (max-width: 992px) { + #sidebar { + margin-inline-start: calc(-1 * var(--sidebar-width)); + position: fixed; + } + #sidebar.active { + margin-inline-start: 0; + } + #content { + width: 100%; + } } diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..a3ebcdc 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,159 @@ - +/* Meezan Accounting Custom Styles */ :root { - --bg-color-start: #6a11cb; - --bg-color-end: #2575fc; - --text-color: #ffffff; - --card-bg-color: rgba(255, 255, 255, 0.01); - --card-border-color: rgba(255, 255, 255, 0.1); + --meezan-primary: #2E5BFF; + --meezan-secondary: #8C9EFF; + --meezan-accent: #FFAB40; + --meezan-bg: #F8FAFF; + --meezan-sidebar-bg: #FFFFFF; + --meezan-text: #1A202C; + --meezan-glass: rgba(255, 255, 255, 0.7); + --sidebar-width: 260px; } + body { - margin: 0; - font-family: 'Inter', sans-serif; - background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); - color: var(--text-color); - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; - text-align: center; - overflow: hidden; - position: relative; + font-family: 'Plus Jakarta Sans', 'Cairo', sans-serif; + background-color: var(--meezan-bg); + color: var(--meezan-text); + overflow-x: hidden; +} + +/* RTL Specific Tweaks */ +[dir="rtl"] { + font-family: 'Cairo', 'Plus Jakarta Sans', sans-serif; +} + +.bg-primary { + background-color: var(--meezan-primary) !important; +} + +.text-primary { + color: var(--meezan-primary) !important; +} + +/* Glassmorphism Effect */ +.glass-card { + background: var(--meezan-glass); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 16px; + box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07); +} + +/* Sidebar Styling */ +#wrapper { + display: flex; + width: 100%; + align-items: stretch; +} + +#sidebar { + min-width: var(--sidebar-width); + max-width: var(--sidebar-width); + background: var(--meezan-sidebar-bg); + color: var(--meezan-text); + transition: all 0.3s; + border-inline-end: 1px solid rgba(0,0,0,0.05); + z-index: 1000; + position: sticky; + top: 0; + height: 100vh; +} + +#sidebar.active { + margin-inline-start: calc(-1 * var(--sidebar-width)); +} + +#sidebar .sidebar-header { + padding: 20px; + background: var(--meezan-sidebar-bg); +} + +#sidebar ul.components { + padding: 20px 0; +} + +#sidebar ul li a { + padding: 12px 25px; + font-size: 1rem; + display: flex; + align-items: center; + color: var(--meezan-text); + text-decoration: none; + transition: all 0.3s; + border-radius: 0 50px 50px 0; + margin-inline-end: 15px; +} + +[dir="rtl"] #sidebar ul li a { + border-radius: 50px 0 0 50px; + margin-inline-start: 15px; + margin-inline-end: 0; +} + +#sidebar ul li a:hover, #sidebar ul li a.active { + background: rgba(46, 91, 255, 0.1); + color: var(--meezan-primary); + font-weight: 600; +} + +#sidebar ul li a i { + margin-inline-end: 15px; + font-size: 1.2rem; +} + +/* Main Content Styling */ +#content { + width: 100%; + padding: 0; + min-height: 100vh; + transition: all 0.3s; +} + +.top-navbar { + padding: 15px 30px; + background: #fff; + border-bottom: 1px solid rgba(0,0,0,0.05); +} + +/* Hero Section */ +.hero-gradient { + background: linear-gradient(135deg, var(--meezan-primary) 0%, var(--meezan-secondary) 100%); + border-radius: 24px; + padding: 3rem; + color: white; + position: relative; + overflow: hidden; +} + +/* Buttons */ +.btn-primary { + background-color: var(--meezan-primary); + border: none; + padding: 0.6rem 1.5rem; + border-radius: 10px; + font-weight: 600; + transition: all 0.3s ease; +} + +/* Dashboard Stats */ +.stat-card { + transition: transform 0.3s ease; +} + +.stat-card:hover { + transform: translateY(-5px); +} + +/* Mobile Tweaks */ +@media (max-width: 992px) { + #sidebar { + margin-inline-start: calc(-1 * var(--sidebar-width)); + position: fixed; + } + #sidebar.active { + margin-inline-start: 0; + } + #content { + width: 100%; + } }