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 @@
+ {% if user.is_authenticated %} + {% endif %} -
+
@@ -170,7 +170,7 @@ {% if sale.payments.exists %} -
+
{% trans "Payment Records" %} / سجلات الدفع
@@ -179,6 +179,7 @@ + @@ -186,8 +187,15 @@ {% for payment in sale.payments.all %} - + + {% endfor %} @@ -199,13 +207,13 @@ {% if sale.notes %} -
+
{% trans "Internal Notes" %} / ملاحظات داخلية

{{ sale.notes }}

{% endif %} -
+

{% trans "Thank you for your business!" %} / شكراً لتعاملكم معنا!

{% trans "Software by Meezan" %} / برمجة ميزان

diff --git a/core/templates/core/invoices.html b/core/templates/core/invoices.html index 9ac35a1..a72ff3b 100644 --- a/core/templates/core/invoices.html +++ b/core/templates/core/invoices.html @@ -36,8 +36,7 @@
- - + @@ -51,13 +50,10 @@ - - {% empty %} - diff --git a/core/templates/core/pos.html b/core/templates/core/pos.html index 28d08db..dc07a9f 100644 --- a/core/templates/core/pos.html +++ b/core/templates/core/pos.html @@ -175,6 +175,16 @@ {% trans "Total" %}{{ site_settings.currency_symbol }}0.000 + +
+ + +
+ @@ -352,11 +362,15 @@ payBtn.disabled = true; payBtn.innerText = '{% trans "Processing..." %}'; + const totalAmount = cart.reduce((acc, item) => acc + item.line_total, 0); const data = { customer_id: document.getElementById('customerSelect').value, + payment_method_id: document.getElementById('paymentMethodSelect').value, items: cart, - total_amount: cart.reduce((acc, item) => acc + item.line_total, 0), - discount: 0 + total_amount: totalAmount, + paid_amount: totalAmount, + discount: 0, + payment_type: 'cash' }; fetch('{% url "create_sale_api" %}', { @@ -470,4 +484,4 @@ }); } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/templates/core/purchase_create.html b/core/templates/core/purchase_create.html index 9170ad7..9892f93 100644 --- a/core/templates/core/purchase_create.html +++ b/core/templates/core/purchase_create.html @@ -122,6 +122,15 @@ +
+ + +
+
@@ -177,6 +186,7 @@ supplierId: '', invoiceNumber: '', paymentType: 'cash', + paymentMethodId: '{% if payment_methods.first %}{{ payment_methods.first.id }}{% endif %}', paidAmount: 0, dueDate: '', notes: '', @@ -243,6 +253,7 @@ total_amount: this.subtotal, paid_amount: actualPaidAmount, payment_type: this.paymentType, + payment_method_id: this.paymentMethodId, due_date: this.dueDate, notes: this.notes }; @@ -272,4 +283,4 @@ } }).mount('#purchaseApp'); -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/purchase_detail.html b/core/templates/core/purchase_detail.html index d515b0e..4472c90 100644 --- a/core/templates/core/purchase_detail.html +++ b/core/templates/core/purchase_detail.html @@ -48,11 +48,15 @@
{{ purchase.invoice_number|default:purchase.id }}
-
+
{% trans "Issue Date" %} / تاريخ الإصدار
{{ purchase.created_at|date:"Y-m-d" }}
-
+
+
{% trans "Issued By" %} / صادرة عن
+
{{ purchase.created_by.username|default:"System" }}
+
+
{% trans "Due Date" %} / تاريخ الاستحقاق
{{ purchase.due_date|date:"Y-m-d"|default:"-" }}
@@ -152,7 +156,7 @@ {% if purchase.payments.exists %} -
+
{% trans "Payment History" %} / سجل الدفعات
{% trans "Date" %} / التاريخ {% trans "Method" %} / الطريقة {% trans "Amount" %} / المبلغ{% trans "User" %} / المستخدم {% trans "Notes" %} / ملاحظات
{{ payment.payment_date|date:"Y-m-d" }}{{ payment.payment_method }} + {% if payment.payment_method %} + {% if LANGUAGE_CODE == 'ar' %}{{ payment.payment_method.name_ar }}{% else %}{{ payment.payment_method.name_en }}{% endif %} + {% else %} + {{ payment.payment_method_name }} + {% endif %} + {{ settings.currency_symbol }}{{ payment.amount|floatformat:3 }}{{ payment.created_by.username|default:"System" }} {{ payment.notes }}
{% trans "Date" %} {% trans "Customer" %} {% trans "Total" %}{% trans "Paid" %}{% trans "Balance" %}{% trans "User" %} {% trans "Status" %} {% trans "Actions" %}
{{ sale.created_at|date:"Y-m-d" }} {{ sale.customer.name|default:_("Guest") }} {{ site_settings.currency_symbol }}{{ sale.total_amount|floatformat:3 }}{{ site_settings.currency_symbol }}{{ sale.paid_amount|floatformat:3 }} - {% if sale.balance_due > 0 %} - {{ site_settings.currency_symbol }}{{ sale.balance_due|floatformat:3 }} - {% else %} - 0.000 - {% endif %} + + + {{ sale.created_by.username|default:"System" }} + {% if sale.status == 'paid' %} @@ -103,10 +99,10 @@
- + {% for method in payment_methods %} + + {% endfor %}
@@ -145,7 +141,7 @@
+ Empty

{% trans "No sales invoices found." %}

@@ -161,6 +165,7 @@ + @@ -168,8 +173,15 @@ {% for payment in purchase.payments.all %} - + + {% endfor %} @@ -181,13 +193,13 @@ {% if purchase.notes %} -
+
{% trans "Notes" %} / ملاحظات

{{ purchase.notes }}

{% endif %} -
+

{% trans "Thank you for your business!" %} / شكراً لتعاملكم معنا!

diff --git a/core/templates/core/purchase_returns.html b/core/templates/core/purchase_returns.html index 5376828..5829777 100644 --- a/core/templates/core/purchase_returns.html +++ b/core/templates/core/purchase_returns.html @@ -35,8 +35,8 @@
- + @@ -48,16 +48,12 @@ - + - - + @@ -51,13 +50,10 @@ - - {% empty %} - diff --git a/core/templates/core/quotations.html b/core/templates/core/quotations.html index ce04f89..e8d5a1e 100644 --- a/core/templates/core/quotations.html +++ b/core/templates/core/quotations.html @@ -36,6 +36,7 @@ + @@ -50,6 +51,11 @@ + {% empty %} - diff --git a/core/templates/core/sales_returns.html b/core/templates/core/sales_returns.html index 838249d..38da94d 100644 --- a/core/templates/core/sales_returns.html +++ b/core/templates/core/sales_returns.html @@ -35,8 +35,8 @@ - + @@ -48,16 +48,12 @@ - +
{% trans "Date" %} / التاريخ {% trans "Method" %} / الطريقة {% trans "Amount" %} / المبلغ{% trans "User" %} / المستخدم {% trans "Notes" %} / ملاحظات
{{ payment.payment_date|date:"Y-m-d" }}{{ payment.payment_method }} + {% if payment.payment_method %} + {% if LANGUAGE_CODE == 'ar' %}{{ payment.payment_method.name_ar }}{% else %}{{ payment.payment_method.name_en }}{% endif %} + {% else %} + {{ payment.payment_method_name }} + {% endif %} + {{ settings.currency_symbol }}{{ payment.amount|floatformat:3 }}{{ payment.created_by.username|default:"System" }} {{ payment.notes }}
{% trans "Return #" %} {% trans "Date" %} {% trans "Supplier" %}{% trans "Original Purchase" %} {% trans "Total Amount" %}{% trans "User" %} {% trans "Actions" %}
{{ return.created_at|date:"Y-m-d" }} {{ return.supplier.name|default:"N/A" }} - {% if return.purchase %} - - #{{ return.purchase.invoice_number|default:return.purchase.id }} - - {% else %} - N/A - {% endif %} - {{ site_settings.currency_symbol }}{{ return.total_amount|floatformat:3 }} + + {{ return.created_by.username|default:"System" }} + + {% trans "Date" %} {% trans "Supplier" %} {% trans "Total" %}{% trans "Paid" %}{% trans "Balance" %}{% trans "User" %} {% trans "Status" %} {% trans "Actions" %}
{{ purchase.created_at|date:"Y-m-d" }} {{ purchase.supplier.name|default:"-" }} {{ site_settings.currency_symbol }}{{ purchase.total_amount|floatformat:3 }}{{ site_settings.currency_symbol }}{{ purchase.paid_amount|floatformat:3 }} - {% if purchase.balance_due > 0 %} - {{ site_settings.currency_symbol }}{{ purchase.balance_due|floatformat:3 }} - {% else %} - 0.000 - {% endif %} + + + {{ purchase.created_by.username|default:"System" }} + {% if purchase.status == 'paid' %} @@ -103,10 +99,10 @@
- + {% for method in payment_methods %} + + {% endfor %}
@@ -145,7 +141,7 @@
+ Empty

{% trans "No purchases recorded yet." %}

{% trans "Date" %} {% trans "Customer" %} {% trans "Total" %}{% trans "User" %} {% trans "Status" %} {% trans "Valid Until" %} {% trans "Actions" %}{{ q.created_at|date:"Y-m-d" }} {{ q.customer.name|default:_("Guest") }} {{ site_settings.currency_symbol }}{{ q.total_amount|floatformat:3 }} + + {{ q.created_by.username|default:"System" }} + + {% if q.status == 'draft' %} {% trans "Draft" %} @@ -120,7 +126,7 @@
+ Empty

{% trans "No quotations found." %}

{% trans "Return #" %} {% trans "Date" %} {% trans "Customer" %}{% trans "Original Sale" %} {% trans "Total Amount" %}{% trans "User" %} {% trans "Actions" %}
{{ return.created_at|date:"Y-m-d" }} {{ return.customer.name|default:_("Guest") }} - {% if return.sale %} - - #{{ return.sale.invoice_number|default:return.sale.id }} - - {% else %} - N/A - {% endif %} - {{ site_settings.currency_symbol }}{{ return.total_amount|floatformat:3 }} + + {{ return.created_by.username|default:"System" }} + + {% endif %} -
-
-
-
-
{% trans "Business Profile" %}
-
-
-
- {% csrf_token %} -
-
- - {% if settings.logo %} - Logo - {% else %} -
- -
- {% endif %} - -
+ -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
- -
{% trans "Financial Preferences" %}
-
- - -
{% trans "e.g., OMR, $, £, SAR" %}
-
-
- - -
+
+ +
+ - -
-
-
-
{% trans "Help & Support" %}
+ + +
+ - -
-
- -
{% trans "Smart Admin Version" %}
-

v2.1.0-Meezan

+
+
+ + + + + + + + + + + {% for pm in payment_methods %} + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Name (EN)" %}{% trans "Name (AR)" %}{% trans "Status" %}{% trans "Actions" %}
{{ pm.name_en }}{{ pm.name_ar }} + {% if pm.is_active %} + {% trans "Active" %} + {% else %} + {% trans "Inactive" %} + {% endif %} + + + +
+
+ + {% trans "No payment methods found." %} +
+
+
+ + + + +{% endblock %} + +{% block extra_js %} + {% endblock %} \ No newline at end of file diff --git a/core/templates/core/users.html b/core/templates/core/users.html new file mode 100644 index 0000000..91e568b --- /dev/null +++ b/core/templates/core/users.html @@ -0,0 +1,119 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "User Management" %} - {{ site_settings.business_name }}{% endblock %} + +{% block content %} +
+

{% trans "User Management" %}

+ +
+ +
+
+
+ + + + + + + + + + + + + {% for u in users %} + + + + + + + + + {% endfor %} + +
{% trans "Username" %}{% trans "Email" %}{% trans "Role/Group" %}{% trans "Status" %}{% trans "Last Login" %}{% trans "Actions" %}
+
+
+ +
+
+
{{ u.username }}
+ {% if u.is_superuser %}Superuser{% endif %} +
+
+
{{ u.email|default:"-" }} + {% for group in u.groups.all %} + {{ group.name }} + {% empty %} + No Role + {% endfor %} + + {% if u.is_active %} + {% trans "Active" %} + {% else %} + {% trans "Inactive" %} + {% endif %} + {{ u.last_login|date:"Y-m-d H:i"|default:"Never" }} +
+ {% csrf_token %} + + + +
+
+
+
+
+ + + +{% endblock %} diff --git a/core/templates/registration/login.html b/core/templates/registration/login.html new file mode 100644 index 0000000..52b45f2 --- /dev/null +++ b/core/templates/registration/login.html @@ -0,0 +1,56 @@ +{% extends 'base.html' %} +{% load static i18n %} + +{% block title %}{% trans "Login" %} - {{ site_settings.business_name }}{% endblock %} + +{% block content %} +
+
+
+

{% trans "Welcome Back" %}

+

{% trans "Please login to access your dashboard" %}

+
+ + {% if form.errors %} + + {% endif %} + +
+ {% csrf_token %} +
+ +
+ + +
+
+
+ +
+ + +
+
+ + +
+ +
+

{% trans "Need help? Contact your administrator." %}

+
+
+
+ + +{% endblock %} diff --git a/core/urls.py b/core/urls.py index 4c768d2..2770656 100644 --- a/core/urls.py +++ b/core/urls.py @@ -10,6 +10,7 @@ urlpatterns = [ path('purchases/', views.purchases, name='purchases'), path('reports/', views.reports, name='reports'), path('settings/', views.settings_view, name='settings'), + path('users/', views.user_management, name='user_management'), # Invoices (Sales) path('invoices/', views.invoice_list, name='invoices'), @@ -80,4 +81,9 @@ urlpatterns = [ path('inventory/unit/edit//', views.edit_unit, name='edit_unit'), path('inventory/unit/delete//', views.delete_unit, name='delete_unit'), path('api/add-unit-ajax/', views.add_unit_ajax, name='add_unit_ajax'), -] \ No newline at end of file + + # Payment Methods + path('settings/payment-methods/add/', views.add_payment_method, name='add_payment_method'), + path('settings/payment-methods/edit//', views.edit_payment_method, name='edit_payment_method'), + path('settings/payment-methods/delete//', views.delete_payment_method, name='delete_payment_method'), +] diff --git a/core/views.py b/core/views.py index 1617e4c..bf82a9d 100644 --- a/core/views.py +++ b/core/views.py @@ -1,3 +1,4 @@ +from django.urls import reverse import random import string from django.shortcuts import render, get_object_or_404, redirect @@ -5,12 +6,14 @@ 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 django.contrib.auth.decorators import login_required from .models import ( Product, Sale, Category, Unit, Customer, Supplier, Purchase, PurchaseItem, PurchasePayment, SaleItem, SalePayment, SystemSetting, Quotation, QuotationItem, - SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem + SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem, + PaymentMethod ) import json from datetime import timedelta @@ -19,6 +22,7 @@ from django.contrib import messages from django.utils.text import slugify import openpyxl +@login_required def index(request): """ Enhanced Meezan Dashboard View @@ -33,7 +37,7 @@ def index(request): low_stock_products = Product.objects.filter(stock_quantity__lt=5) # Recent Transactions - recent_sales = Sale.objects.order_by('-created_at')[:5] + recent_sales = Sale.objects.order_by('-created_at').select_related('created_by')[:5] # Chart Data: Sales for the last 7 days seven_days_ago = timezone.now().date() - timedelta(days=6) @@ -65,31 +69,45 @@ def index(request): } return render(request, 'core/index.html', context) +@login_required def inventory(request): products = Product.objects.all().select_related('category', 'unit', 'supplier') categories = Category.objects.all() - units = Unit.objects.all() suppliers = Supplier.objects.all() context = { 'products': products, 'categories': categories, - 'units': units, 'suppliers': suppliers } return render(request, 'core/inventory.html', context) +@login_required def pos(request): products = Product.objects.all().filter(stock_quantity__gt=0, is_active=True) customers = Customer.objects.all() categories = Category.objects.all() - context = {'products': products, 'customers': customers, 'categories': categories} + payment_methods = PaymentMethod.objects.filter(is_active=True) + + # Ensure at least Cash exists + if not payment_methods.exists(): + PaymentMethod.objects.create(name_en="Cash", name_ar="نقدي", is_active=True) + payment_methods = PaymentMethod.objects.filter(is_active=True) + + context = { + 'products': products, + 'customers': customers, + 'categories': categories, + 'payment_methods': payment_methods + } return render(request, 'core/pos.html', context) +@login_required def customers(request): customers_list = Customer.objects.all().annotate(total_sales=Sum('sales__total_amount')) context = {'customers': customers_list} return render(request, 'core/customers.html', context) +@login_required def suppliers(request): suppliers_list = Supplier.objects.all() context = {'suppliers': suppliers_list} @@ -97,23 +115,37 @@ def suppliers(request): # --- Purchase Views --- +@login_required def purchases(request): - purchases_list = Purchase.objects.all().select_related('supplier').order_by('-created_at') + purchases_list = Purchase.objects.all().select_related('supplier', 'created_by').order_by('-created_at') suppliers_list = Supplier.objects.all() - context = {'purchases': purchases_list, 'suppliers': suppliers_list} + payment_methods = PaymentMethod.objects.filter(is_active=True) + context = { + 'purchases': purchases_list, + 'suppliers': suppliers_list, + 'payment_methods': payment_methods + } return render(request, 'core/purchases.html', context) +@login_required def purchase_create(request): products = Product.objects.filter(is_active=True) suppliers = Supplier.objects.all() - return render(request, 'core/purchase_create.html', {'products': products, 'suppliers': suppliers}) + payment_methods = PaymentMethod.objects.filter(is_active=True) + return render(request, 'core/purchase_create.html', { + 'products': products, + 'suppliers': suppliers, + 'payment_methods': payment_methods + }) +@login_required def purchase_detail(request, pk): purchase = get_object_or_404(Purchase, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/purchase_detail.html', {'purchase': purchase, 'settings': settings}) @csrf_exempt +@login_required def create_purchase_api(request): if request.method == 'POST': try: @@ -124,6 +156,7 @@ def create_purchase_api(request): total_amount = data.get('total_amount', 0) paid_amount = data.get('paid_amount', 0) payment_type = data.get('payment_type', 'cash') + payment_method_id = data.get('payment_method_id') due_date = data.get('due_date') notes = data.get('notes', '') @@ -139,7 +172,8 @@ def create_purchase_api(request): balance_due=float(total_amount) - float(paid_amount), payment_type=payment_type, due_date=due_date if due_date else None, - notes=notes + notes=notes, + created_by=request.user ) # Set status based on payments @@ -153,11 +187,17 @@ def create_purchase_api(request): # Record the initial payment if any if float(paid_amount) > 0: + pm = None + if payment_method_id: + pm = PaymentMethod.objects.filter(id=payment_method_id).first() + PurchasePayment.objects.create( purchase=purchase, amount=paid_amount, - payment_method=payment_type.capitalize(), - notes=_("Initial payment") + payment_method=pm, + payment_method_name=pm.name_en if pm else payment_type.capitalize(), + notes="Initial payment", + created_by=request.user ) for item in items: @@ -179,25 +219,33 @@ def create_purchase_api(request): return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) +@login_required def add_purchase_payment(request, pk): purchase = get_object_or_404(Purchase, pk=pk) if request.method == 'POST': amount = request.POST.get('amount') payment_date = request.POST.get('payment_date', timezone.now().date()) - payment_method = request.POST.get('payment_method', 'Cash') + payment_method_id = request.POST.get('payment_method_id') notes = request.POST.get('notes', '') + pm = None + if payment_method_id: + pm = PaymentMethod.objects.filter(id=payment_method_id).first() + PurchasePayment.objects.create( purchase=purchase, amount=amount, payment_date=payment_date, - payment_method=payment_method, - notes=notes + payment_method=pm, + payment_method_name=pm.name_en if pm else "Cash", + notes=notes, + created_by=request.user ) purchase.update_balance() - messages.success(request, _("Payment added successfully!")) + messages.success(request, "Payment added successfully!") return redirect('purchases') +@login_required def delete_purchase(request, pk): purchase = get_object_or_404(Purchase, pk=pk) for item in purchase.items.all(): @@ -205,27 +253,41 @@ def delete_purchase(request, pk): item.product.save() purchase.delete() - messages.success(request, _("Purchase deleted successfully!")) + messages.success(request, "Purchase deleted successfully!") return redirect('purchases') # --- Sale Views --- +@login_required def invoice_list(request): - sales = Sale.objects.all().order_by('-created_at') + sales = Sale.objects.all().select_related('customer', 'created_by').order_by('-created_at') customers = Customer.objects.all() - return render(request, 'core/invoices.html', {'sales': sales, 'customers': customers}) + payment_methods = PaymentMethod.objects.filter(is_active=True) + return render(request, 'core/invoices.html', { + 'sales': sales, + 'customers': customers, + 'payment_methods': payment_methods + }) +@login_required def invoice_create(request): products = Product.objects.filter(is_active=True) customers = Customer.objects.all() - return render(request, 'core/invoice_create.html', {'products': products, 'customers': customers}) + payment_methods = PaymentMethod.objects.filter(is_active=True) + return render(request, 'core/invoice_create.html', { + 'products': products, + 'customers': customers, + 'payment_methods': payment_methods + }) +@login_required def invoice_detail(request, pk): sale = get_object_or_404(Sale, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/invoice_detail.html', {'sale': sale, 'settings': settings}) @csrf_exempt +@login_required def create_sale_api(request): if request.method == 'POST': try: @@ -237,6 +299,7 @@ def create_sale_api(request): paid_amount = data.get('paid_amount', 0) discount = data.get('discount', 0) payment_type = data.get('payment_type', 'cash') + payment_method_id = data.get('payment_method_id') due_date = data.get('due_date') notes = data.get('notes', '') @@ -253,7 +316,8 @@ def create_sale_api(request): discount=discount, payment_type=payment_type, due_date=due_date if due_date else None, - notes=notes + notes=notes, + created_by=request.user ) # Set status based on payments @@ -267,11 +331,17 @@ def create_sale_api(request): # Record initial payment if any if float(paid_amount) > 0: + pm = None + if payment_method_id: + pm = PaymentMethod.objects.filter(id=payment_method_id).first() + SalePayment.objects.create( sale=sale, amount=paid_amount, - payment_method=payment_type.capitalize(), - notes=_("Initial payment") + payment_method=pm, + payment_method_name=pm.name_en if pm else payment_type.capitalize(), + notes="Initial payment", + created_by=request.user ) for item in items: @@ -325,52 +395,64 @@ def create_sale_api(request): return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) +@login_required def add_sale_payment(request, pk): sale = get_object_or_404(Sale, pk=pk) if request.method == 'POST': amount = request.POST.get('amount') payment_date = request.POST.get('payment_date', timezone.now().date()) - payment_method = request.POST.get('payment_method', 'Cash') + payment_method_id = request.POST.get('payment_method_id') notes = request.POST.get('notes', '') + pm = None + if payment_method_id: + pm = PaymentMethod.objects.filter(id=payment_method_id).first() + SalePayment.objects.create( sale=sale, amount=amount, payment_date=payment_date, - payment_method=payment_method, - notes=notes + payment_method=pm, + payment_method_name=pm.name_en if pm else "Cash", + notes=notes, + created_by=request.user ) sale.update_balance() - messages.success(request, _("Payment added successfully!")) + messages.success(request, "Payment added successfully!") return redirect('invoices') +@login_required def delete_sale(request, pk): sale = get_object_or_404(Sale, pk=pk) for item in sale.items.all(): item.product.stock_quantity += item.quantity item.product.save() sale.delete() - messages.success(request, _("Sale deleted successfully!")) + messages.success(request, "Sale deleted successfully!") return redirect('invoices') # --- Quotation Views --- +@login_required def quotations(request): - quotations_list = Quotation.objects.all().order_by('-created_at') + quotations_list = Quotation.objects.all().select_related('customer', 'created_by').order_by('-created_at') customers = Customer.objects.all() return render(request, 'core/quotations.html', {'quotations': quotations_list, 'customers': customers}) +@login_required def quotation_create(request): products = Product.objects.filter(is_active=True) customers = Customer.objects.all() return render(request, 'core/quotation_create.html', {'products': products, 'customers': customers}) +@login_required def quotation_detail(request, pk): quotation = get_object_or_404(Quotation, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/quotation_detail.html', {'quotation': quotation, 'settings': settings}) @csrf_exempt +@login_required def create_quotation_api(request): if request.method == 'POST': try: @@ -395,7 +477,8 @@ def create_quotation_api(request): discount=discount, valid_until=valid_until if valid_until else None, terms_and_conditions=terms_and_conditions, - notes=notes + notes=notes, + created_by=request.user ) for item in items: @@ -413,10 +496,11 @@ def create_quotation_api(request): return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) +@login_required def convert_quotation_to_invoice(request, pk): quotation = get_object_or_404(Quotation, pk=pk) if quotation.status == 'converted': - messages.warning(request, _("This quotation has already been converted to an invoice.")) + messages.warning(request, "This quotation has already been converted to an invoice.") return redirect('invoices') # Create Sale from Quotation @@ -428,7 +512,8 @@ def convert_quotation_to_invoice(request, pk): balance_due=quotation.total_amount, payment_type='cash', status='unpaid', - notes=quotation.notes + notes=quotation.notes, + created_by=request.user ) # Create SaleItems and Update Stock @@ -448,21 +533,24 @@ def convert_quotation_to_invoice(request, pk): quotation.status = 'converted' quotation.save() - messages.success(request, _("Quotation converted to Invoice successfully!")) + messages.success(request, "Quotation converted to Invoice successfully!") return redirect('invoice_detail', pk=sale.pk) +@login_required def delete_quotation(request, pk): quotation = get_object_or_404(Quotation, pk=pk) quotation.delete() - messages.success(request, _("Quotation deleted successfully!")) + messages.success(request, "Quotation deleted successfully!") return redirect('quotations') # --- Sale Return Views --- +@login_required def sales_returns(request): - returns = SaleReturn.objects.all().order_by('-created_at') + returns = SaleReturn.objects.all().select_related('customer', 'created_by').order_by('-created_at') return render(request, 'core/sales_returns.html', {'returns': returns}) +@login_required def sale_return_create(request): products = Product.objects.filter(is_active=True) customers = Customer.objects.all() @@ -473,12 +561,14 @@ def sale_return_create(request): 'sales': sales }) +@login_required def sale_return_detail(request, pk): sale_return = get_object_or_404(SaleReturn, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/sale_return_detail.html', {'sale_return': sale_return, 'settings': settings}) @csrf_exempt +@login_required def create_sale_return_api(request): if request.method == 'POST': try: @@ -503,7 +593,8 @@ def create_sale_return_api(request): customer=customer, return_number=return_number, total_amount=total_amount, - notes=notes + notes=notes, + created_by=request.user ) for item in items: @@ -524,22 +615,25 @@ def create_sale_return_api(request): return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) +@login_required def delete_sale_return(request, pk): sale_return = get_object_or_404(SaleReturn, pk=pk) for item in sale_return.items.all(): item.product.stock_quantity -= item.quantity item.product.save() sale_return.delete() - messages.success(request, _("Sale return deleted successfully!")) + messages.success(request, "Sale return deleted successfully!") return redirect('sales_returns') # --- Purchase Return Views --- +@login_required def purchase_returns(request): - returns = PurchaseReturn.objects.all().order_by('-created_at') + returns = PurchaseReturn.objects.all().select_related('supplier', 'created_by').order_by('-created_at') return render(request, 'core/purchase_returns.html', {'returns': returns}) +@login_required def purchase_return_create(request): products = Product.objects.filter(is_active=True) suppliers = Supplier.objects.all() @@ -550,12 +644,14 @@ def purchase_return_create(request): 'purchases': purchases }) +@login_required def purchase_return_detail(request, pk): purchase_return = get_object_or_404(PurchaseReturn, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/purchase_return_detail.html', {'purchase_return': purchase_return, 'settings': settings}) @csrf_exempt +@login_required def create_purchase_return_api(request): if request.method == 'POST': try: @@ -580,7 +676,8 @@ def create_purchase_return_api(request): supplier=supplier, return_number=return_number, total_amount=total_amount, - notes=notes + notes=notes, + created_by=request.user ) for item in items: @@ -601,17 +698,19 @@ def create_purchase_return_api(request): return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) +@login_required def delete_purchase_return(request, pk): purchase_return = get_object_or_404(PurchaseReturn, pk=pk) for item in purchase_return.items.all(): item.product.stock_quantity += item.quantity item.product.save() purchase_return.delete() - messages.success(request, _("Purchase return deleted successfully!")) + messages.success(request, "Purchase return deleted successfully!") return redirect('purchase_returns') # --- Other Management Views --- +@login_required def reports(request): """ Smart Reports View @@ -633,6 +732,7 @@ def reports(request): } return render(request, 'core/reports.html', context) +@login_required def settings_view(request): """ Smart Admin Settings View @@ -658,8 +758,42 @@ def settings_view(request): messages.success(request, "Settings updated successfully!") return redirect('settings') - return render(request, 'core/settings.html', {'settings': settings}) + payment_methods = PaymentMethod.objects.all() + + return render(request, 'core/settings.html', { + 'settings': settings, + 'payment_methods': payment_methods, + }) +@login_required +def add_payment_method(request): + if request.method == 'POST': + name_en = request.POST.get('name_en') + name_ar = request.POST.get('name_ar') + is_active = request.POST.get('is_active') == 'on' + PaymentMethod.objects.create(name_en=name_en, name_ar=name_ar, is_active=is_active) + messages.success(request, "Payment method added successfully!") + return redirect('settings') + +@login_required +def edit_payment_method(request, pk): + pm = get_object_or_404(PaymentMethod, pk=pk) + if request.method == 'POST': + pm.name_en = request.POST.get('name_en') + pm.name_ar = request.POST.get('name_ar') + pm.is_active = request.POST.get('is_active') == 'on' + pm.save() + messages.success(request, "Payment method updated successfully!") + return redirect('settings') + +@login_required +def delete_payment_method(request, pk): + pm = get_object_or_404(PaymentMethod, pk=pk) + pm.delete() + messages.success(request, "Payment method deleted successfully!") + return redirect('settings') + +@login_required def add_customer(request): if request.method == 'POST': name = request.POST.get('name') @@ -670,6 +804,7 @@ def add_customer(request): messages.success(request, "Customer added successfully!") return redirect('customers') +@login_required def edit_customer(request, pk): customer = get_object_or_404(Customer, pk=pk) if request.method == 'POST': @@ -681,12 +816,14 @@ def edit_customer(request, pk): messages.success(request, "Customer updated successfully!") return redirect('customers') +@login_required def delete_customer(request, pk): customer = get_object_or_404(Customer, pk=pk) customer.delete() messages.success(request, "Customer deleted successfully!") return redirect('customers') +@login_required def add_supplier(request): if request.method == 'POST': name = request.POST.get('name') @@ -696,6 +833,7 @@ def add_supplier(request): messages.success(request, "Supplier added successfully!") return redirect('suppliers') +@login_required def edit_supplier(request, pk): supplier = get_object_or_404(Supplier, pk=pk) if request.method == 'POST': @@ -706,6 +844,7 @@ def edit_supplier(request, pk): messages.success(request, "Supplier updated successfully!") return redirect('suppliers') +@login_required def delete_supplier(request, pk): supplier = get_object_or_404(Supplier, pk=pk) supplier.delete() @@ -713,6 +852,7 @@ def delete_supplier(request, pk): return redirect('suppliers') +@login_required def suggest_sku(request): """ API endpoint to suggest a unique SKU. @@ -723,6 +863,7 @@ def suggest_sku(request): if not Product.objects.filter(sku=sku).exists(): return JsonResponse({"sku": sku}) +@login_required def add_product(request): if request.method == 'POST': name_en = request.POST.get('name_en') @@ -767,8 +908,9 @@ def add_product(request): product.save() messages.success(request, "Product added successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#items') +@login_required def edit_product(request, pk): product = get_object_or_404(Product, pk=pk) if request.method == 'POST': @@ -795,15 +937,17 @@ def edit_product(request, pk): product.save() messages.success(request, "Product updated successfully!") - return redirect('inventory') - return redirect('inventory') + return redirect(reverse('inventory') + '#items') + return redirect(reverse('inventory') + '#items') +@login_required def delete_product(request, pk): product = get_object_or_404(Product, pk=pk) product.delete() messages.success(request, "Product deleted successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#items') +@login_required def add_category(request): if request.method == 'POST': name_en = request.POST.get('name_en') @@ -811,8 +955,9 @@ def add_category(request): slug = slugify(name_en) Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug) messages.success(request, "Category added successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#categories-list') +@login_required def edit_category(request, pk): category = get_object_or_404(Category, pk=pk) if request.method == 'POST': @@ -821,14 +966,16 @@ def edit_category(request, pk): category.slug = slugify(category.name_en) category.save() messages.success(request, "Category updated successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#categories-list') +@login_required def delete_category(request, pk): category = get_object_or_404(Category, pk=pk) category.delete() messages.success(request, "Category deleted successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#categories-list') +@login_required def add_unit(request): if request.method == 'POST': name_en = request.POST.get('name_en') @@ -836,8 +983,9 @@ def add_unit(request): short_name = request.POST.get('short_name') Unit.objects.create(name_en=name_en, name_ar=name_ar, short_name=short_name) messages.success(request, "Unit added successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#units-list') +@login_required def edit_unit(request, pk): unit = get_object_or_404(Unit, pk=pk) if request.method == 'POST': @@ -846,19 +994,22 @@ def edit_unit(request, pk): unit.short_name = request.POST.get('short_name') unit.save() messages.success(request, "Unit updated successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#units-list') +@login_required def delete_unit(request, pk): unit = get_object_or_404(Unit, pk=pk) unit.delete() messages.success(request, "Unit deleted successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#units-list') +@login_required def barcode_labels(request): products = Product.objects.filter(is_active=True).order_by('name_en') context = {'products': products} return render(request, 'core/barcode_labels.html', context) +@login_required def import_products(request): """ Import products from an Excel (.xlsx) file. @@ -869,7 +1020,7 @@ def import_products(request): if not excel_file.name.endswith('.xlsx'): messages.error(request, "Please upload a valid .xlsx file.") - return redirect('inventory') + return redirect(reverse('inventory') + '#items') try: wb = openpyxl.load_workbook(excel_file) @@ -938,9 +1089,10 @@ def import_products(request): except Exception as e: messages.error(request, f"Error processing file: {str(e)}") - return redirect('inventory') + return redirect(reverse('inventory') + '#items') @csrf_exempt +@login_required def add_category_ajax(request): if request.method == 'POST': try: @@ -963,6 +1115,7 @@ def add_category_ajax(request): return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) @csrf_exempt +@login_required def add_unit_ajax(request): if request.method == 'POST': try: @@ -985,6 +1138,7 @@ def add_unit_ajax(request): return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) @csrf_exempt +@login_required def add_supplier_ajax(request): if request.method == 'POST': try: @@ -1004,3 +1158,46 @@ def add_supplier_ajax(request): except Exception as e: return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) + +@login_required +def user_management(request): + if not (request.user.is_superuser or request.user.groups.filter(name='admin').exists()): + messages.error(request, "Access denied.") + return redirect('index') + + from django.contrib.auth.models import User, Group + users = User.objects.all().prefetch_related('groups') + groups = Group.objects.all() + + if request.method == 'POST': + action = request.POST.get('action') + if action == 'add': + username = request.POST.get('username') + password = request.POST.get('password') + email = request.POST.get('email') + group_id = request.POST.get('group') + + if User.objects.filter(username=username).exists(): + messages.error(request, "Username already exists.") + else: + user = User.objects.create_user(username=username, email=email, password=password) + if group_id: + group = Group.objects.get(id=group_id) + user.groups.add(group) + user.is_staff = True + user.save() + messages.success(request, f"User {username} created successfully.") + + elif action == 'toggle_status': + user_id = request.POST.get('user_id') + user = get_object_or_404(User, id=user_id) + if user == request.user: + messages.error(request, "You cannot deactivate yourself.") + else: + user.is_active = not user.is_active + user.save() + messages.success(request, f"User {user.username} status updated.") + + return redirect('user_management') + + return render(request, 'core/users.html', {'users': users, 'groups': groups}) \ No newline at end of file diff --git a/static/css/custom.css b/static/css/custom.css index a3ebcdc..d5cb873 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -101,6 +101,43 @@ body { font-size: 1.2rem; } +/* Collapsible Sidebar Styles */ +#sidebar ul.components li.sidebar-group-header > a { + padding: 12px 25px; + font-size: 0.85rem; + text-transform: uppercase; + font-weight: 700; + color: #6c757d; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 0; + margin-inline-end: 0; +} + +#sidebar ul.components li.sidebar-group-header > a:hover { + background: transparent; + color: var(--meezan-primary); +} + +#sidebar ul.components li.sidebar-group-header > a i.chevron { + transition: transform 0.3s; +} + +#sidebar ul.components li.sidebar-group-header > a[aria-expanded="true"] i.chevron { + transform: rotate(180deg); +} + +#sidebar ul.sub-menu li a { + padding-inline-start: 50px; + font-size: 0.95rem; +} + +[dir="rtl"] #sidebar ul.sub-menu li a { + padding-inline-end: 50px; + padding-inline-start: 25px; +} + /* Main Content Styling */ #content { width: 100%; @@ -156,4 +193,4 @@ body { #content { width: 100%; } -} +} \ No newline at end of file diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index a3ebcdc..d5cb873 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -101,6 +101,43 @@ body { font-size: 1.2rem; } +/* Collapsible Sidebar Styles */ +#sidebar ul.components li.sidebar-group-header > a { + padding: 12px 25px; + font-size: 0.85rem; + text-transform: uppercase; + font-weight: 700; + color: #6c757d; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 0; + margin-inline-end: 0; +} + +#sidebar ul.components li.sidebar-group-header > a:hover { + background: transparent; + color: var(--meezan-primary); +} + +#sidebar ul.components li.sidebar-group-header > a i.chevron { + transition: transform 0.3s; +} + +#sidebar ul.components li.sidebar-group-header > a[aria-expanded="true"] i.chevron { + transform: rotate(180deg); +} + +#sidebar ul.sub-menu li a { + padding-inline-start: 50px; + font-size: 0.95rem; +} + +[dir="rtl"] #sidebar ul.sub-menu li a { + padding-inline-end: 50px; + padding-inline-start: 25px; +} + /* Main Content Styling */ #content { width: 100%; @@ -156,4 +193,4 @@ body { #content { width: 100%; } -} +} \ No newline at end of file