diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 88d5360..ab578e6 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 1b7a2c8..fe03dcc 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 443b50a..35aeb66 100644 --- a/config/settings.py +++ b/config/settings.py @@ -186,7 +186,13 @@ CONTACT_EMAIL_TO = [ # When both TLS and SSL flags are enabled, prefer SSL explicitly if EMAIL_USE_SSL: EMAIL_USE_TLS = False + +# Authentication +LOGIN_REDIRECT_URL = '/' +LOGOUT_REDIRECT_URL = '/accounts/login/' +LOGIN_URL = '/accounts/login/' + # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' \ No newline at end of file +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/config/urls.py b/config/urls.py index 5d88023..22b4d6a 100644 --- a/config/urls.py +++ b/config/urls.py @@ -5,6 +5,7 @@ from django.conf.urls.static import static urlpatterns = [ path("admin/", admin.site.urls), + path("accounts/", include("django.contrib.auth.urls")), path("i18n/", include("django.conf.urls.i18n")), path("", include("core.urls")), ] @@ -12,4 +13,4 @@ urlpatterns = [ 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 + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 7a79afc..13f71e7 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 7392a4c..268182e 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 a5eaed3..a26b97d 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 4700987..f373a8f 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 ada3ad1..b8fac4d 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 a255268..49646aa 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,5 +1,13 @@ from django.contrib import admin -from .models import Category, Unit, Product, Customer, Supplier, Sale, SaleItem, Purchase, SystemSetting +from .models import ( + Category, Unit, Product, Customer, Supplier, + Sale, SaleItem, SalePayment, + Purchase, PurchaseItem, PurchasePayment, + Quotation, QuotationItem, + SaleReturn, SaleReturnItem, + PurchaseReturn, PurchaseReturnItem, + SystemSetting, PaymentMethod +) @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): @@ -13,7 +21,7 @@ class UnitAdmin(admin.ModelAdmin): @admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ('name_en', 'name_ar', 'sku', 'price', 'stock_quantity', 'category', 'unit') - list_filter = ('category', 'unit') + list_filter = ('category', 'unit', 'is_active') search_fields = ('name_en', 'name_ar', 'sku') @admin.register(Customer) @@ -25,19 +33,41 @@ class CustomerAdmin(admin.ModelAdmin): class SupplierAdmin(admin.ModelAdmin): list_display = ('name', 'contact_person', 'phone') +@admin.register(PaymentMethod) +class PaymentMethodAdmin(admin.ModelAdmin): + list_display = ('name_en', 'name_ar', 'is_active') + class SaleItemInline(admin.TabularInline): model = SaleItem extra = 1 +class SalePaymentInline(admin.TabularInline): + model = SalePayment + extra = 1 + @admin.register(Sale) class SaleAdmin(admin.ModelAdmin): - list_display = ('id', 'customer', 'total_amount', 'created_at') - inlines = [SaleItemInline] + list_display = ('id', 'invoice_number', 'customer', 'total_amount', 'paid_amount', 'status', 'created_at') + list_filter = ('status', 'created_at') + inlines = [SaleItemInline, SalePaymentInline] @admin.register(Purchase) class PurchaseAdmin(admin.ModelAdmin): - list_display = ('id', 'supplier', 'total_amount', 'created_at') - list_filter = ('supplier', 'created_at') + list_display = ('id', 'invoice_number', 'supplier', 'total_amount', 'paid_amount', 'status', 'created_at') + list_filter = ('supplier', 'status', 'created_at') + +@admin.register(Quotation) +class QuotationAdmin(admin.ModelAdmin): + list_display = ('quotation_number', 'customer', 'total_amount', 'status', 'created_at') + list_filter = ('status', 'created_at') + +@admin.register(SaleReturn) +class SaleReturnAdmin(admin.ModelAdmin): + list_display = ('return_number', 'customer', 'total_amount', 'created_at') + +@admin.register(PurchaseReturn) +class PurchaseReturnAdmin(admin.ModelAdmin): + list_display = ('return_number', 'supplier', 'total_amount', 'created_at') @admin.register(SystemSetting) class SystemSettingAdmin(admin.ModelAdmin): diff --git a/core/context_processors.py b/core/context_processors.py index 2769ebb..33b1942 100644 --- a/core/context_processors.py +++ b/core/context_processors.py @@ -1,6 +1,10 @@ from .models import SystemSetting import os -from django.utils import timezone +import time + +# Stabilize the timestamp to avoid cache-busting on every single request +# This will only change when the server restarts +STARTUP_TIMESTAMP = int(time.time()) def project_context(request): """ @@ -10,7 +14,7 @@ def project_context(request): return { "project_description": os.getenv("PROJECT_DESCRIPTION", ""), "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), - "deployment_timestamp": int(timezone.now().timestamp()), + "deployment_timestamp": STARTUP_TIMESTAMP, } def global_settings(request): @@ -20,4 +24,4 @@ def global_settings(request): settings = SystemSetting.objects.create() return {'site_settings': settings} except: - return {} + return {} \ No newline at end of file diff --git a/core/migrations/0010_purchase_created_by_purchasepayment_created_by_and_more.py b/core/migrations/0010_purchase_created_by_purchasepayment_created_by_and_more.py new file mode 100644 index 0000000..a36b326 --- /dev/null +++ b/core/migrations/0010_purchase_created_by_purchasepayment_created_by_and_more.py @@ -0,0 +1,51 @@ +# Generated by Django 5.2.7 on 2026-02-02 10:42 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_purchasereturn_purchasereturnitem_salereturn_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='purchase', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchases', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='purchasepayment', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_payments', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='purchasereturn', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_returns', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='quotation', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quotations', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='sale', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='salepayment', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sale_payments', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='salereturn', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sale_returns', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/core/migrations/0011_paymentmethod_purchasepayment_payment_method_name_and_more.py b/core/migrations/0011_paymentmethod_purchasepayment_payment_method_name_and_more.py new file mode 100644 index 0000000..b8298f0 --- /dev/null +++ b/core/migrations/0011_paymentmethod_purchasepayment_payment_method_name_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 5.2.7 on 2026-02-02 13:01 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_purchase_created_by_purchasepayment_created_by_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='PaymentMethod', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_en', models.CharField(max_length=50, verbose_name='Name (English)')), + ('name_ar', models.CharField(max_length=50, verbose_name='Name (Arabic)')), + ('is_active', models.BooleanField(default=True, verbose_name='Active')), + ], + ), + migrations.AddField( + model_name='purchasepayment', + name='payment_method_name', + field=models.CharField(default='Cash', max_length=50, verbose_name='Payment Method Name'), + ), + migrations.AddField( + model_name='salepayment', + name='payment_method_name', + field=models.CharField(default='Cash', max_length=50, verbose_name='Payment Method Name'), + ), + migrations.AlterField( + model_name='purchasepayment', + name='payment_method', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_payments', to='core.paymentmethod'), + ), + migrations.AlterField( + model_name='salepayment', + name='payment_method', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sale_payments', to='core.paymentmethod'), + ), + ] diff --git a/core/migrations/__pycache__/0010_purchase_created_by_purchasepayment_created_by_and_more.cpython-311.pyc b/core/migrations/__pycache__/0010_purchase_created_by_purchasepayment_created_by_and_more.cpython-311.pyc new file mode 100644 index 0000000..44cd063 Binary files /dev/null and b/core/migrations/__pycache__/0010_purchase_created_by_purchasepayment_created_by_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0011_paymentmethod_purchasepayment_payment_method_name_and_more.cpython-311.pyc b/core/migrations/__pycache__/0011_paymentmethod_purchasepayment_payment_method_name_and_more.cpython-311.pyc new file mode 100644 index 0000000..58d28f2 Binary files /dev/null and b/core/migrations/__pycache__/0011_paymentmethod_purchasepayment_payment_method_name_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 2c98bf5..697ec63 100644 --- a/core/models.py +++ b/core/models.py @@ -1,6 +1,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from django.utils import timezone +from django.contrib.auth.models import User class Category(models.Model): name_en = models.CharField(_("Name (English)"), max_length=100) @@ -58,6 +59,14 @@ class Supplier(models.Model): def __str__(self): return self.name +class PaymentMethod(models.Model): + name_en = models.CharField(_("Name (English)"), max_length=50) + name_ar = models.CharField(_("Name (Arabic)"), max_length=50) + is_active = models.BooleanField(_("Active"), default=True) + + def __str__(self): + return f"{self.name_en} / {self.name_ar}" + class Sale(models.Model): PAYMENT_TYPE_CHOICES = [ ('cash', _('Cash')), @@ -81,6 +90,7 @@ class Sale(models.Model): status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='paid') due_date = models.DateField(_("Due Date"), null=True, blank=True) notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="sales") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): @@ -112,8 +122,10 @@ class SalePayment(models.Model): sale = models.ForeignKey(Sale, on_delete=models.CASCADE, related_name="payments") amount = models.DecimalField(_("Amount"), max_digits=15, decimal_places=3) payment_date = models.DateField(_("Payment Date"), default=timezone.now) - payment_method = models.CharField(_("Payment Method"), max_length=50, default="Cash") + payment_method = models.ForeignKey(PaymentMethod, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_payments") + payment_method_name = models.CharField(_("Payment Method Name"), max_length=50, default="Cash") # Fallback notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_payments") def __str__(self): return f"Payment of {self.amount} for Sale #{self.sale.id}" @@ -135,6 +147,7 @@ class Quotation(models.Model): valid_until = models.DateField(_("Valid Until"), null=True, blank=True) terms_and_conditions = models.TextField(_("Terms and Conditions"), blank=True) notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="quotations") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): @@ -171,6 +184,7 @@ class Purchase(models.Model): status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='paid') due_date = models.DateField(_("Due Date"), null=True, blank=True) notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchases") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): @@ -202,8 +216,10 @@ class PurchasePayment(models.Model): purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE, related_name="payments") amount = models.DecimalField(_("Amount"), max_digits=15, decimal_places=3) payment_date = models.DateField(_("Payment Date"), default=timezone.now) - payment_method = models.CharField(_("Payment Method"), max_length=50, default="Cash") + payment_method = models.ForeignKey(PaymentMethod, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_payments") + payment_method_name = models.CharField(_("Payment Method Name"), max_length=50, default="Cash") # Fallback notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_payments") def __str__(self): return f"Payment of {self.amount} for Purchase #{self.purchase.id}" @@ -214,6 +230,7 @@ class SaleReturn(models.Model): return_number = models.CharField(_("Return Number"), max_length=50, blank=True) total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_returns") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): @@ -235,6 +252,7 @@ class PurchaseReturn(models.Model): return_number = models.CharField(_("Return Number"), max_length=50, blank=True) total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_returns") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): diff --git a/core/templates/base.html b/core/templates/base.html index 9b79024..61ca302 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -29,6 +29,7 @@