diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 27ce5b1..125d336 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 29bb55f..ceb4e69 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/edit_product_fixed.py b/core/edit_product_fixed.py new file mode 100644 index 0000000..f64efac --- /dev/null +++ b/core/edit_product_fixed.py @@ -0,0 +1,35 @@ +@login_required +def edit_product(request, pk): + product = get_object_or_404(Product, pk=pk) + if request.method == 'POST': + product.name_en = request.POST.get('name_en') + product.name_ar = request.POST.get('name_ar') + product.sku = request.POST.get('sku') + product.category = get_object_or_404(Category, id=request.POST.get('category')) + + unit_id = request.POST.get('unit') + product.unit = get_object_or_404(Unit, id=unit_id) if unit_id else None + + supplier_id = request.POST.get('supplier') + product.supplier = get_object_or_404(Supplier, id=supplier_id) if supplier_id else None + + product.cost_price = request.POST.get('cost_price', 0) + product.price = request.POST.get('price', 0) + product.vat = request.POST.get('vat', 0) + product.description = request.POST.get('description', '') + product.opening_stock = request.POST.get('opening_stock', 0) + product.stock_quantity = request.POST.get('stock_quantity', 0) + product.min_stock_level = request.POST.get('min_stock_level', 0) + product.is_active = request.POST.get('is_active') == 'on' + product.has_expiry = request.POST.get('has_expiry') == 'on' + product.expiry_date = request.POST.get('expiry_date') + if not product.has_expiry: + product.expiry_date = None + + if 'image' in request.FILES: + product.image = request.FILES['image'] + + product.save() + messages.success(request, _("Product updated successfully!")) + return redirect(reverse('inventory') + '#items') + return redirect(reverse('inventory') + '#items') diff --git a/core/migrations/0020_product_expiry_date_product_has_expiry_and_more.py b/core/migrations/0020_product_expiry_date_product_has_expiry_and_more.py new file mode 100644 index 0000000..c803b72 --- /dev/null +++ b/core/migrations/0020_product_expiry_date_product_has_expiry_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.7 on 2026-02-03 10:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0019_systemsetting_wablas_secret_key'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='expiry_date', + field=models.DateField(blank=True, null=True, verbose_name='Expiry Date'), + ), + migrations.AddField( + model_name='product', + name='has_expiry', + field=models.BooleanField(default=False, verbose_name='Has Expiry Date'), + ), + migrations.AddField( + model_name='purchaseitem', + name='expiry_date', + field=models.DateField(blank=True, null=True, verbose_name='Expiry Date'), + ), + migrations.AddField( + model_name='purchasereturnitem', + name='expiry_date', + field=models.DateField(blank=True, null=True, verbose_name='Expiry Date'), + ), + ] diff --git a/core/migrations/0021_product_min_stock_level.py b/core/migrations/0021_product_min_stock_level.py new file mode 100644 index 0000000..327af1e --- /dev/null +++ b/core/migrations/0021_product_min_stock_level.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-02-03 10:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0020_product_expiry_date_product_has_expiry_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='min_stock_level', + field=models.PositiveIntegerField(default=0, verbose_name='Stock Level (Alert)'), + ), + ] diff --git a/core/migrations/0022_alter_product_min_stock_level_and_more.py b/core/migrations/0022_alter_product_min_stock_level_and_more.py new file mode 100644 index 0000000..f1d7e9c --- /dev/null +++ b/core/migrations/0022_alter_product_min_stock_level_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 5.2.7 on 2026-02-03 10:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0021_product_min_stock_level'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='min_stock_level', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Stock Level (Alert)'), + ), + migrations.AlterField( + model_name='product', + name='opening_stock', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Opening Stock'), + ), + migrations.AlterField( + model_name='product', + name='stock_quantity', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='In Stock'), + ), + migrations.AlterField( + model_name='purchaseitem', + name='quantity', + field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='purchasereturnitem', + name='quantity', + field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='quotationitem', + name='quantity', + field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='saleitem', + name='quantity', + field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='salereturnitem', + name='quantity', + field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'), + ), + ] diff --git a/core/migrations/0023_alter_product_min_stock_level_and_more.py b/core/migrations/0023_alter_product_min_stock_level_and_more.py new file mode 100644 index 0000000..c53fd41 --- /dev/null +++ b/core/migrations/0023_alter_product_min_stock_level_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 5.2.7 on 2026-02-03 10:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0022_alter_product_min_stock_level_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='min_stock_level', + field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Stock Level (Alert)'), + ), + migrations.AlterField( + model_name='product', + name='opening_stock', + field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Opening Stock'), + ), + migrations.AlterField( + model_name='product', + name='stock_quantity', + field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='In Stock'), + ), + migrations.AlterField( + model_name='purchaseitem', + name='quantity', + field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='purchasereturnitem', + name='quantity', + field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='quotationitem', + name='quantity', + field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='saleitem', + name='quantity', + field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='salereturnitem', + name='quantity', + field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'), + ), + ] diff --git a/core/migrations/__pycache__/0020_product_expiry_date_product_has_expiry_and_more.cpython-311.pyc b/core/migrations/__pycache__/0020_product_expiry_date_product_has_expiry_and_more.cpython-311.pyc new file mode 100644 index 0000000..3026924 Binary files /dev/null and b/core/migrations/__pycache__/0020_product_expiry_date_product_has_expiry_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0021_product_min_stock_level.cpython-311.pyc b/core/migrations/__pycache__/0021_product_min_stock_level.cpython-311.pyc new file mode 100644 index 0000000..64c5b46 Binary files /dev/null and b/core/migrations/__pycache__/0021_product_min_stock_level.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0022_alter_product_min_stock_level_and_more.cpython-311.pyc b/core/migrations/__pycache__/0022_alter_product_min_stock_level_and_more.cpython-311.pyc new file mode 100644 index 0000000..c6ec414 Binary files /dev/null and b/core/migrations/__pycache__/0022_alter_product_min_stock_level_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0023_alter_product_min_stock_level_and_more.cpython-311.pyc b/core/migrations/__pycache__/0023_alter_product_min_stock_level_and_more.cpython-311.pyc new file mode 100644 index 0000000..26d8ee4 Binary files /dev/null and b/core/migrations/__pycache__/0023_alter_product_min_stock_level_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index a0d518a..48f0c54 100644 --- a/core/models.py +++ b/core/models.py @@ -35,8 +35,11 @@ class Product(models.Model): cost_price = models.DecimalField(_("Cost Price"), max_digits=12, decimal_places=3, default=0) price = models.DecimalField(_("Sale Price"), max_digits=12, decimal_places=3) vat = models.DecimalField(_("VAT (%)"), max_digits=5, decimal_places=2, default=0) - opening_stock = models.PositiveIntegerField(_("Opening Stock"), default=0) - stock_quantity = models.PositiveIntegerField(_("In Stock"), default=0) + opening_stock = models.DecimalField(_("Opening Stock"), max_digits=15, decimal_places=2, default=0) + stock_quantity = models.DecimalField(_("In Stock"), max_digits=15, decimal_places=2, default=0) + min_stock_level = models.DecimalField(_("Stock Level (Alert)"), max_digits=15, decimal_places=2, default=0) + has_expiry = models.BooleanField(_("Has Expiry Date"), default=False) + expiry_date = models.DateField(_("Expiry Date"), null=True, blank=True) image = models.ImageField(_("Product Image"), upload_to="product_images/", blank=True, null=True) is_active = models.BooleanField(_("Active"), default=True) created_at = models.DateTimeField(auto_now_add=True) @@ -175,7 +178,7 @@ class Sale(models.Model): 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(_("Quantity")) + quantity = models.DecimalField(_("Quantity"), max_digits=15, decimal_places=2) unit_price = models.DecimalField(_("Unit Price"), max_digits=12, decimal_places=3) line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3) @@ -220,7 +223,7 @@ class Quotation(models.Model): class QuotationItem(models.Model): quotation = models.ForeignKey(Quotation, on_delete=models.CASCADE, related_name="items") product = models.ForeignKey(Product, on_delete=models.CASCADE) - quantity = models.PositiveIntegerField(_("Quantity")) + quantity = models.DecimalField(_("Quantity"), max_digits=15, decimal_places=2) unit_price = models.DecimalField(_("Unit Price"), max_digits=12, decimal_places=3) line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3) @@ -269,8 +272,9 @@ class Purchase(models.Model): class PurchaseItem(models.Model): purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE, related_name="items") product = models.ForeignKey(Product, on_delete=models.CASCADE) - quantity = models.PositiveIntegerField(_("Quantity")) + quantity = models.DecimalField(_("Quantity"), max_digits=15, decimal_places=2) cost_price = models.DecimalField(_("Cost Price"), max_digits=12, decimal_places=3) + expiry_date = models.DateField(_("Expiry Date"), null=True, blank=True) line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3) def __str__(self): @@ -303,7 +307,7 @@ class SaleReturn(models.Model): class SaleReturnItem(models.Model): sale_return = models.ForeignKey(SaleReturn, on_delete=models.CASCADE, related_name="items") product = models.ForeignKey(Product, on_delete=models.CASCADE) - quantity = models.PositiveIntegerField(_("Quantity")) + quantity = models.DecimalField(_("Quantity"), max_digits=15, decimal_places=2) unit_price = models.DecimalField(_("Unit Price"), max_digits=12, decimal_places=3) line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3) @@ -325,8 +329,9 @@ class PurchaseReturn(models.Model): class PurchaseReturnItem(models.Model): purchase_return = models.ForeignKey(PurchaseReturn, on_delete=models.CASCADE, related_name="items") product = models.ForeignKey(Product, on_delete=models.CASCADE) - quantity = models.PositiveIntegerField(_("Quantity")) + quantity = models.DecimalField(_("Quantity"), max_digits=15, decimal_places=2) cost_price = models.DecimalField(_("Cost Price"), max_digits=12, decimal_places=3) + expiry_date = models.DateField(_("Expiry Date"), null=True, blank=True) line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3) def __str__(self): @@ -388,4 +393,4 @@ def create_user_profile(sender, instance, created, **kwargs): def save_user_profile(sender, instance, **kwargs): if not hasattr(instance, 'profile'): UserProfile.objects.create(user=instance) - instance.profile.save() \ No newline at end of file + instance.profile.save() diff --git a/core/templates/core/index.html b/core/templates/core/index.html index cfcfc72..4e085ac 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -117,6 +117,22 @@

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

{% endif %} +
{% trans "Expired Items Alert" %}
+ {% if expired_count > 0 %} +
+ +
+

{{ expired_count }} {% trans "Items have expired!" %}

+ {% trans "View and manage expired stock" %} +
+
+ {% else %} +
+ +

{% trans "No expired items in stock." %}

+
+ {% endif %} +
{% trans "View Full Inventory" %}
diff --git a/core/templates/core/inventory.html b/core/templates/core/inventory.html index 19f84ef..39f1e0e 100644 --- a/core/templates/core/inventory.html +++ b/core/templates/core/inventory.html @@ -59,6 +59,11 @@ {% trans "Units" %} +
@@ -127,8 +132,8 @@ {{ product.sku }} {{ product.category.name_ar }} / {{ product.category.name_en }} - - {{ product.stock_quantity }} {{ product.unit.short_name|default:"" }} + + {{ product.stock_quantity|floatformat:2 }} {{ product.unit.short_name|default:"" }} {{ site_settings.currency_symbol }}{{ product.cost_price|floatformat:3 }} @@ -154,6 +159,157 @@
+ + + + + + {% empty %} @@ -250,6 +406,71 @@ + + +
+
+
+
+
+
{% trans "Expired Items" %}
+

{{ expired_products.count }}

+
+
+
+
+
+
+
{% trans "Expiring within 30 days" %}
+

{{ expiring_soon_products.count }}

+
+
+
+
+ +
+
+ + + + + + + + + + + + {% for product in expired_products %} + + + + + + + + {% empty %} + {% endfor %} + {% for product in expiring_soon_products %} + + + + + + + + {% empty %} + {% if not expired_products %} + + + + {% endif %} + {% endfor %} + +
{% trans "Item" %}{% trans "SKU" %}{% trans "Stock" %}{% trans "Expiry Date" %}{% trans "Status" %}
{{ product.name_ar }} / {{ product.name_en }}{{ product.sku }}{{ product.stock_quantity|floatformat:2 }} {{ product.unit.short_name }}{{ product.expiry_date|date:"Y-m-d" }}{% trans "Expired" %}
{{ product.name_ar }} / {{ product.name_en }}{{ product.sku }}{{ product.stock_quantity|floatformat:2 }} {{ product.unit.short_name }}{{ product.expiry_date|date:"Y-m-d" }}{% trans "Expiring Soon" %}
{% trans "No expired or expiring items found." %}
+
+
+
@@ -363,6 +584,15 @@ {% endfor %} +
+ + +
@@ -373,7 +603,43 @@
- + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ @@ -440,7 +706,7 @@ const response = await fetch('{% url "add_category_ajax" %}', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' }, body: JSON.stringify({ name_en: nameEn, name_ar: nameAr }) }); const data = await response.json(); @@ -465,7 +731,7 @@ const response = await fetch('{% url "add_unit_ajax" %}', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' }, body: JSON.stringify({ name_en: nameEn, name_ar: nameAr, short_name: shortName }) }); const data = await response.json(); @@ -488,6 +754,15 @@ document.getElementById('saveUnit').onclick = () => saveUnit(false); document.getElementById('saveAndAddAnotherUnit').onclick = () => saveUnit(true); + // Expiry Date Toggle + const hasExpiryCheck = document.getElementById('hasExpiryCheck'); + const expiryDateDiv = document.getElementById('expiryDateDiv'); + if (hasExpiryCheck && expiryDateDiv) { + hasExpiryCheck.onchange = function() { + expiryDateDiv.style.display = this.checked ? 'block' : 'none'; + }; + } + // SKU Suggestion const suggestBtn = document.getElementById('suggestSkuBtn'); if (suggestBtn) { @@ -499,4 +774,4 @@ } }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/templates/core/invoice_create.html b/core/templates/core/invoice_create.html index 134a336..ed6b6d7 100644 --- a/core/templates/core/invoice_create.html +++ b/core/templates/core/invoice_create.html @@ -75,7 +75,7 @@ - + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] diff --git a/core/templates/core/invoice_detail.html b/core/templates/core/invoice_detail.html index 4f96ac3..058b6dd 100644 --- a/core/templates/core/invoice_detail.html +++ b/core/templates/core/invoice_detail.html @@ -126,7 +126,7 @@
{{ item.product.name_en }}
{{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }} - {{ item.quantity }} + {{ item.quantity|floatformat:2 }} {{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }} {% endfor %} diff --git a/core/templates/core/invoice_edit.html b/core/templates/core/invoice_edit.html index 64b76f3..9634dff 100644 --- a/core/templates/core/invoice_edit.html +++ b/core/templates/core/invoice_edit.html @@ -80,7 +80,7 @@ - + [[ currencySymbol ]][[ (parseFloat(item.price) * parseFloat(item.quantity)).toFixed(decimalPlaces) ]] diff --git a/core/templates/core/pos.html b/core/templates/core/pos.html index 32da2e9..2be11e6 100644 --- a/core/templates/core/pos.html +++ b/core/templates/core/pos.html @@ -598,11 +598,11 @@
${item.name_ar}
${item.name_en}
-
${currency} ${formatAmount(item.price)} x ${item.quantity}
+
${currency} ${formatAmount(item.price)} x ${parseFloat(item.quantity).toFixed(2)}
- ${item.quantity} + ${parseFloat(item.quantity).toFixed(2)}
@@ -850,7 +850,7 @@
${item.name_ar}
${item.name_en}
- ${item.qty} + ${parseFloat(item.qty).toFixed(2)} ${data.business.currency} ${formatAmount(item.total)} `; diff --git a/core/templates/core/purchase_create.html b/core/templates/core/purchase_create.html index 6769ba5..d01fae9 100644 --- a/core/templates/core/purchase_create.html +++ b/core/templates/core/purchase_create.html @@ -60,6 +60,7 @@ {% trans "Product" %} {% trans "Cost Price" %} + {% trans "Expiry Date" %} {% trans "Quantity" %} {% trans "Total" %} @@ -75,7 +76,10 @@ - + + + + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] @@ -224,7 +228,8 @@ name_en: product.name_en, sku: product.sku, price: product.cost_price, - quantity: 1 + quantity: 1, + expiry_date: "" }); } this.searchQuery = ''; @@ -251,6 +256,7 @@ id: item.id, quantity: item.quantity, price: item.price, + expiry_date: item.expiry_date, line_total: item.price * item.quantity })), total_amount: this.subtotal, diff --git a/core/templates/core/purchase_detail.html b/core/templates/core/purchase_detail.html index 149e2b6..f91ae88 100644 --- a/core/templates/core/purchase_detail.html +++ b/core/templates/core/purchase_detail.html @@ -120,7 +120,7 @@
{{ item.product.name_en }}
{{ settings.currency_symbol }}{{ item.cost_price|floatformat:3 }} - {{ item.quantity }} + {{ item.quantity|floatformat:2 }} {{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }} {% endfor %} diff --git a/core/templates/core/purchase_return_create.html b/core/templates/core/purchase_return_create.html index 1cbc84f..99efd1c 100644 --- a/core/templates/core/purchase_return_create.html +++ b/core/templates/core/purchase_return_create.html @@ -84,7 +84,7 @@ - + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] diff --git a/core/templates/core/purchase_return_detail.html b/core/templates/core/purchase_return_detail.html index 7b44615..2e2961a 100644 --- a/core/templates/core/purchase_return_detail.html +++ b/core/templates/core/purchase_return_detail.html @@ -98,7 +98,7 @@
{{ item.product.name_en }}
{{ settings.currency_symbol }}{{ item.cost_price|floatformat:3 }} - {{ item.quantity }} + {{ item.quantity|floatformat:2 }} {{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }} {% endfor %} diff --git a/core/templates/core/quotation_create.html b/core/templates/core/quotation_create.html index 3acd4f0..0450d79 100644 --- a/core/templates/core/quotation_create.html +++ b/core/templates/core/quotation_create.html @@ -75,7 +75,7 @@ - + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] diff --git a/core/templates/core/quotation_detail.html b/core/templates/core/quotation_detail.html index b92dfcc..748ee79 100644 --- a/core/templates/core/quotation_detail.html +++ b/core/templates/core/quotation_detail.html @@ -142,7 +142,7 @@
{{ item.product.name_en }}
{{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }} - {{ item.quantity }} + {{ item.quantity|floatformat:2 }} {{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }} {% endfor %} diff --git a/core/templates/core/sale_return_create.html b/core/templates/core/sale_return_create.html index 81e9e96..d605dc1 100644 --- a/core/templates/core/sale_return_create.html +++ b/core/templates/core/sale_return_create.html @@ -84,7 +84,7 @@ - + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] diff --git a/core/templates/core/sale_return_detail.html b/core/templates/core/sale_return_detail.html index 88a9fe9..04197e7 100644 --- a/core/templates/core/sale_return_detail.html +++ b/core/templates/core/sale_return_detail.html @@ -98,7 +98,7 @@
{{ item.product.name_en }}
{{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }} - {{ item.quantity }} + {{ item.quantity|floatformat:2 }} {{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }} {% endfor %} diff --git a/core/views.py b/core/views.py index 22c79d0..fb5606a 100644 --- a/core/views.py +++ b/core/views.py @@ -11,7 +11,7 @@ from django.urls import reverse import random import string from django.shortcuts import render, get_object_or_404, redirect -from django.db.models import Sum, Count, F +from django.db.models import Sum, Count, F, Q from django.db.models.functions import TruncDate, TruncMonth from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt @@ -42,6 +42,10 @@ def index(request): total_sales_amount = Sale.objects.aggregate(total=Sum('total_amount'))['total'] or 0 total_customers = Customer.objects.count() + # Expired Items Alert + today = timezone.now().date() + expired_count = Product.objects.filter(has_expiry=True, expiry_date__lt=today, stock_quantity__gt=0).count() + # Stock Alert (Low stock < 5) low_stock_products = Product.objects.filter(stock_quantity__lt=5) @@ -71,7 +75,7 @@ def index(request): 'total_sales_count': total_sales_count, 'total_sales_amount': total_sales_amount, 'total_customers': total_customers, - 'low_stock_products': low_stock_products, + 'low_stock_products': low_stock_products, 'expired_count': expired_count, 'recent_sales': recent_sales, 'chart_labels': json.dumps(chart_labels), 'chart_data': json.dumps(chart_data), @@ -81,17 +85,42 @@ def index(request): @login_required def inventory(request): products_list = Product.objects.all().select_related('category', 'unit', 'supplier').order_by('-created_at') + + # Filter by category + category_id = request.GET.get('category') + if category_id: + products_list = products_list.filter(category_id=category_id) + + # Search + search = request.GET.get('search') + if search: + products_list = products_list.filter( + Q(name_en__icontains=search) | + Q(name_ar__icontains=search) | + Q(sku__icontains=search) + ) + + # Expired items + today = timezone.now().date() + expired_products = Product.objects.filter(has_expiry=True, expiry_date__lt=today, stock_quantity__gt=0) + expiring_soon_products = Product.objects.filter(has_expiry=True, expiry_date__gte=today, expiry_date__lte=today + timedelta(days=30), stock_quantity__gt=0) + paginator = Paginator(products_list, 25) page_number = request.GET.get('page') products = paginator.get_page(page_number) + categories = Category.objects.all() suppliers = Supplier.objects.all() units = Unit.objects.all() + context = { 'products': products, 'categories': categories, 'suppliers': suppliers, - 'units': units + 'units': units, + 'expired_products': expired_products, + 'expiring_soon_products': expiring_soon_products, + 'today': today } return render(request, 'core/inventory.html', context) @@ -241,16 +270,24 @@ def create_purchase_api(request): for item in items: product = Product.objects.get(id=item['id']) + item_expiry = item.get('expiry_date') PurchaseItem.objects.create( purchase=purchase, product=product, quantity=item['quantity'], cost_price=item['price'], + expiry_date=item_expiry if item_expiry else None, line_total=item['line_total'] ) # Update Stock product.stock_quantity += int(item['quantity']) product.cost_price = item['price'] + + if item_expiry: + product.has_expiry = True + if not product.expiry_date or str(item_expiry) > str(product.expiry_date): + product.expiry_date = item_expiry + product.save() return JsonResponse({'success': True, 'purchase_id': purchase.id}) @@ -996,6 +1033,10 @@ def add_payment_method(request): name_en = request.POST.get('name_en') name_ar = request.POST.get('name_ar') is_active = request.POST.get('is_active') == 'on' + has_expiry = request.POST.get('has_expiry') == 'on' + expiry_date = request.POST.get('expiry_date') + if not has_expiry: + expiry_date = None PaymentMethod.objects.create(name_en=name_en, name_ar=name_ar, is_active=is_active) messages.success(request, _("Payment method added successfully!")) return redirect(reverse('settings') + '#payments') @@ -1105,9 +1146,14 @@ def add_product(request): cost_price = request.POST.get('cost_price', 0) price = request.POST.get('price', 0) vat = request.POST.get('vat', 0) + description = request.POST.get('description', '') opening_stock = request.POST.get('opening_stock', 0) stock_quantity = request.POST.get('stock_quantity', 0) is_active = request.POST.get('is_active') == 'on' + has_expiry = request.POST.get('has_expiry') == 'on' + expiry_date = request.POST.get('expiry_date') + if not has_expiry: + expiry_date = None category = get_object_or_404(Category, id=category_id) unit = get_object_or_404(Unit, id=unit_id) if unit_id else None @@ -1123,9 +1169,13 @@ def add_product(request): cost_price=cost_price, price=price, vat=vat, + description=description, opening_stock=opening_stock, stock_quantity=stock_quantity, - is_active=is_active + is_active=is_active, + has_expiry=has_expiry, + min_stock_level=request.POST.get('min_stock_level', 0), + expiry_date=expiry_date ) if 'image' in request.FILES: @@ -1153,9 +1203,15 @@ def edit_product(request, pk): product.cost_price = request.POST.get('cost_price', 0) product.price = request.POST.get('price', 0) product.vat = request.POST.get('vat', 0) + product.description = request.POST.get('description', '') product.opening_stock = request.POST.get('opening_stock', 0) product.stock_quantity = request.POST.get('stock_quantity', 0) + product.min_stock_level = request.POST.get('min_stock_level', 0) product.is_active = request.POST.get('is_active') == 'on' + product.has_expiry = request.POST.get('has_expiry') == 'on' + product.expiry_date = request.POST.get('expiry_date') + if not product.has_expiry: + product.expiry_date = None if 'image' in request.FILES: product.image = request.FILES['image'] @@ -1165,6 +1221,8 @@ def edit_product(request, pk): return redirect(reverse('inventory') + '#items') 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) diff --git a/core/views_patch.py b/core/views_patch.py new file mode 100644 index 0000000..f64efac --- /dev/null +++ b/core/views_patch.py @@ -0,0 +1,35 @@ +@login_required +def edit_product(request, pk): + product = get_object_or_404(Product, pk=pk) + if request.method == 'POST': + product.name_en = request.POST.get('name_en') + product.name_ar = request.POST.get('name_ar') + product.sku = request.POST.get('sku') + product.category = get_object_or_404(Category, id=request.POST.get('category')) + + unit_id = request.POST.get('unit') + product.unit = get_object_or_404(Unit, id=unit_id) if unit_id else None + + supplier_id = request.POST.get('supplier') + product.supplier = get_object_or_404(Supplier, id=supplier_id) if supplier_id else None + + product.cost_price = request.POST.get('cost_price', 0) + product.price = request.POST.get('price', 0) + product.vat = request.POST.get('vat', 0) + product.description = request.POST.get('description', '') + product.opening_stock = request.POST.get('opening_stock', 0) + product.stock_quantity = request.POST.get('stock_quantity', 0) + product.min_stock_level = request.POST.get('min_stock_level', 0) + product.is_active = request.POST.get('is_active') == 'on' + product.has_expiry = request.POST.get('has_expiry') == 'on' + product.expiry_date = request.POST.get('expiry_date') + if not product.has_expiry: + product.expiry_date = None + + if 'image' in request.FILES: + product.image = request.FILES['image'] + + product.save() + messages.success(request, _("Product updated successfully!")) + return redirect(reverse('inventory') + '#items') + return redirect(reverse('inventory') + '#items')