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 %}
+
+ {% else %}
+
+
+
{% trans "No expired items in stock." %}
+
+ {% endif %}
+
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 @@
+
+
+
+
+
+
+
+
+
+ {% if product.image %}
+

+ {% else %}
+
+
+
+ {% endif %}
+
+
+
{{ product.name_ar }}
+
{{ product.name_en }}
+
+
+
{% trans "SKU" %}: {{ product.sku }}
+
{% trans "Category" %}: {{ product.category.name_ar }}
+
{% trans "Stock" %}: {{ product.stock_quantity|floatformat:2 }} {{ product.unit.short_name }} (Min: {{ product.min_stock_level|floatformat:2 }})
+
{% trans "Price" %}: {{ site_settings.currency_symbol }}{{ product.price }}
+
{% trans "VAT" %}: {{ product.vat }}%
+
{% trans "Supplier" %}: {{ product.supplier.name|default:"N/A" }}
+ {% if product.has_expiry %}
+
{% trans "Expiry Date" %}: {{ product.expiry_date|date:"Y-m-d" }}
+ {% endif %}
+
{% trans "Description" %}:
{{ product.description|linebreaks }}
+
+
+
+
+
+
+
+
+
+
{% empty %}
@@ -250,6 +406,71 @@
+
+
+
+
+
+
+
+ {% trans "Expired Items" %}
+ {{ expired_products.count }}
+
+
+
+
+
+
+ {% trans "Expiring within 30 days" %}
+ {{ expiring_soon_products.count }}
+
+
+
+
+
+
+
+
+
+
+ | {% trans "Item" %} |
+ {% trans "SKU" %} |
+ {% trans "Stock" %} |
+ {% trans "Expiry Date" %} |
+ {% trans "Status" %} |
+
+
+
+ {% for product in expired_products %}
+
+ | {{ 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" %} |
+
+ {% empty %}
+ {% endfor %}
+ {% for product in expiring_soon_products %}
+
+ | {{ 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" %} |
+
+ {% empty %}
+ {% if not expired_products %}
+
+ | {% trans "No expired or expiring items found." %} |
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
@@ -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')