enhancing items form

This commit is contained in:
Flatlogic Bot 2026-02-03 10:34:50 +00:00
parent e473add476
commit 3f9709efef
28 changed files with 620 additions and 33 deletions

View File

@ -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')

View File

@ -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'),
),
]

View File

@ -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)'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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()
instance.profile.save()

View File

@ -117,6 +117,22 @@
<p class="mt-3 text-muted">{% trans "All stock levels are healthy!" %}</p>
</div>
{% endif %}
<h5 class="fw-bold mb-4 mt-4">{% trans "Expired Items Alert" %}</h5>
{% if expired_count > 0 %}
<div class="alert alert-danger border-0 rounded-4 d-flex align-items-center mb-0">
<i class="bi bi-exclamation-triangle-fill fs-4 me-3"></i>
<div>
<p class="mb-0 fw-bold">{{ expired_count }} {% trans "Items have expired!" %}</p>
<a href="{% url 'inventory' %}#expired-list" class="alert-link small">{% trans "View and manage expired stock" %}</a>
</div>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-shield-check text-success display-6"></i>
<p class="mt-2 text-muted small">{% trans "No expired items in stock." %}</p>
</div>
{% endif %}
<div class="mt-auto pt-3 border-top">
<a href="{% url 'inventory' %}" class="btn btn-light btn-sm w-100 fw-bold">{% trans "View Full Inventory" %}</a>
</div>

View File

@ -59,6 +59,11 @@
<i class="bi bi-rulers me-2"></i>{% trans "Units" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link px-4 rounded-3 text-danger" id="expired-tab" data-bs-toggle="tab" data-bs-target="#expired-list" type="button" role="tab">
<i class="bi bi-calendar-x me-2"></i>{% trans "Expired / Expiring" %}
</button>
</li>
</ul>
<div class="tab-content" id="stockTabsContent">
@ -127,8 +132,8 @@
<td><code>{{ product.sku }}</code></td>
<td>{{ product.category.name_ar }} / {{ product.category.name_en }}</td>
<td>
<span class="badge {% if product.stock_quantity < 5 %}bg-danger{% else %}bg-success-subtle text-success{% endif %} rounded-pill">
{{ product.stock_quantity }} {{ product.unit.short_name|default:"" }}
<span class="badge {% if product.stock_quantity <= product.min_stock_level %}bg-danger{% else %}bg-success-subtle text-success{% endif %} rounded-pill">
{{ product.stock_quantity|floatformat:2 }} {{ product.unit.short_name|default:"" }}
</span>
</td>
<td>{{ site_settings.currency_symbol }}{{ product.cost_price|floatformat:3 }}</td>
@ -154,6 +159,157 @@
</div>
</td>
</tr>
<!-- View Product Modal -->
<div class="modal fade" id="viewProductModal{{ product.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Item Details" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-md-4">
{% if product.image %}
<img src="{{ product.image.url }}" class="img-fluid rounded-4 shadow-sm" alt="">
{% else %}
<div class="bg-light rounded-4 d-flex align-items-center justify-content-center" style="height: 200px;">
<i class="bi bi-box-seam text-muted opacity-25" style="font-size: 4rem;"></i>
</div>
{% endif %}
</div>
<div class="col-md-8">
<h3 class="fw-bold text-dark" dir="rtl">{{ product.name_ar }}</h3>
<h5 class="text-muted">{{ product.name_en }}</h5>
<hr>
<div class="row">
<div class="col-6 mb-2"><strong>{% trans "SKU" %}:</strong> <code>{{ product.sku }}</code></div>
<div class="col-6 mb-2"><strong>{% trans "Category" %}:</strong> {{ product.category.name_ar }}</div>
<div class="col-6 mb-2"><strong>{% trans "Stock" %}:</strong> {{ product.stock_quantity|floatformat:2 }} {{ product.unit.short_name }} (Min: {{ product.min_stock_level|floatformat:2 }})</div>
<div class="col-6 mb-2"><strong>{% trans "Price" %}:</strong> {{ site_settings.currency_symbol }}{{ product.price }}</div>
<div class="col-6 mb-2"><strong>{% trans "VAT" %}:</strong> {{ product.vat }}%</div>
<div class="col-6 mb-2"><strong>{% trans "Supplier" %}:</strong> {{ product.supplier.name|default:"N/A" }}</div>
{% if product.has_expiry %}
<div class="col-12 mb-2"><strong>{% trans "Expiry Date" %}:</strong> <span class="text-danger fw-bold">{{ product.expiry_date|date:"Y-m-d" }}</span></div>
{% endif %}
<div class="col-12 mt-2"><strong>{% trans "Description" %}:</strong><br>{{ product.description|linebreaks }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Product Modal -->
<div class="modal fade" id="editProductModal{{ product.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Edit Item" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{% url 'edit_product' product.id %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div class="modal-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Name (English)" %}</label>
<input type="text" name="name_en" class="form-control rounded-3" value="{{ product.name_en }}" required>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Name (Arabic)" %}</label>
<input type="text" name="name_ar" class="form-control rounded-3" dir="rtl" value="{{ product.name_ar }}" required>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Barcode / SKU" %}</label>
<input type="text" name="sku" class="form-control rounded-3" value="{{ product.sku }}">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Category" %}</label>
<select name="category" class="form-select rounded-3" required>
{% for category in categories %}
<option value="{{ category.id }}" {% if category == product.category %}selected{% endif %}>{{ category.name_ar }} / {{ category.name_en }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Unit" %}</label>
<select name="unit" class="form-select rounded-3">
<option value="">{% trans "Select Unit" %}</option>
{% for unit in units %}
<option value="{{ unit.id }}" {% if unit == product.unit %}selected{% endif %}>{{ unit.name_ar }} / {{ unit.name_en }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Supplier" %}</label>
<select name="supplier" class="form-select rounded-3">
<option value="">{% trans "Select Supplier" %}</option>
{% for supplier in suppliers %}
<option value="{{ supplier.id }}" {% if supplier == product.supplier %}selected{% endif %}>{{ supplier.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Cost Price" %}</label>
<input type="number" step="0.001" name="cost_price" class="form-control rounded-3" value="{{ product.cost_price|floatformat:3 }}" required>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Sale Price" %}</label>
<input type="number" step="0.001" name="price" class="form-control rounded-3" value="{{ product.price|floatformat:3 }}" required>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Stock" %}</label>
<input type="number" name="stock_quantity" class="form-control rounded-3" step="0.01" value="{{ product.stock_quantity|floatformat:2 }}" required>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Stock Level (Alert)" %}</label>
<input type="number" name="min_stock_level" class="form-control rounded-3" step="0.01" value="{{ product.min_stock_level|floatformat:2 }}">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Opening Stock" %}</label>
<input type="number" name="opening_stock" class="form-control rounded-3" step="0.01" value="{{ product.opening_stock|floatformat:2 }}">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "VAT (%)" %}</label>
<input type="number" step="0.01" name="vat" class="form-control rounded-3" value="{{ product.vat|floatformat:2 }}">
</div>
<div class="col-md-4">
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" name="is_active" id="isActiveCheck{{ product.id }}" {% if product.is_active %}checked{% endif %}>
<label class="form-check-label small fw-bold" for="isActiveCheck{{ product.id }}">{% trans "Active" %}</label>
</div>
</div>
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Description" %}</label>
<textarea name="description" class="form-control rounded-3" rows="2">{{ product.description }}</textarea>
</div>
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Update Image" %}</label>
<input type="file" name="image" class="form-control rounded-3" accept="image/*">
</div>
<div class="col-md-4">
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" name="has_expiry" id="hasExpiryCheck{{ product.id }}" {% if product.has_expiry %}checked{% endif %} onchange="document.getElementById('expiryDateDiv{{ product.id }}').style.display = this.checked ? 'block' : 'none'">
<label class="form-check-label small fw-bold" for="hasExpiryCheck{{ product.id }}">{% trans "Has Expiry Date" %}</label>
</div>
</div>
<div class="col-md-4" id="expiryDateDiv{{ product.id }}" style="display: {% if product.has_expiry %}block{% else %}none{% endif %};">
<label class="form-label small fw-bold">{% trans "Expiry Date" %}</label>
<input type="date" name="expiry_date" class="form-control rounded-3" value="{{ product.expiry_date|date:'Y-m-d' }}">
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Save Changes" %}</button>
</div>
</form>
</div>
</div>
</div>
{% empty %}
<tr>
<td colspan="8" class="text-center py-5">
@ -250,6 +406,71 @@
</div>
</div>
</div>
<!-- Expired Items Tab -->
<div class="tab-pane fade" id="expired-list" role="tabpanel">
<div class="row mb-4">
<div class="col-md-6">
<div class="card border-0 shadow-sm rounded-4 bg-danger text-white">
<div class="card-body">
<h6 class="fw-bold mb-1">{% trans "Expired Items" %}</h6>
<h2 class="mb-0">{{ expired_products.count }}</h2>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-0 shadow-sm rounded-4 bg-warning text-dark">
<div class="card-body">
<h6 class="fw-bold mb-1">{% trans "Expiring within 30 days" %}</h6>
<h2 class="mb-0">{{ expiring_soon_products.count }}</h2>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">{% trans "Item" %}</th>
<th>{% trans "SKU" %}</th>
<th>{% trans "Stock" %}</th>
<th>{% trans "Expiry Date" %}</th>
<th>{% trans "Status" %}</th>
</tr>
</thead>
<tbody>
{% for product in expired_products %}
<tr>
<td class="ps-4 fw-bold text-dark">{{ product.name_ar }} / {{ product.name_en }}</td>
<td><code>{{ product.sku }}</code></td>
<td>{{ product.stock_quantity|floatformat:2 }} {{ product.unit.short_name }}</td>
<td class="text-danger fw-bold">{{ product.expiry_date|date:"Y-m-d" }}</td>
<td><span class="badge bg-danger">{% trans "Expired" %}</span></td>
</tr>
{% empty %}
{% endfor %}
{% for product in expiring_soon_products %}
<tr>
<td class="ps-4 fw-bold text-dark">{{ product.name_ar }} / {{ product.name_en }}</td>
<td><code>{{ product.sku }}</code></td>
<td>{{ product.stock_quantity|floatformat:2 }} {{ product.unit.short_name }}</td>
<td class="text-warning fw-bold">{{ product.expiry_date|date:"Y-m-d" }}</td>
<td><span class="badge bg-warning text-dark">{% trans "Expiring Soon" %}</span></td>
</tr>
{% empty %}
{% if not expired_products %}
<tr>
<td colspan="5" class="text-center py-4 text-muted">{% trans "No expired or expiring items found." %}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@ -363,6 +584,15 @@
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Supplier" %}</label>
<select name="supplier" class="form-select rounded-3">
<option value="">{% trans "Select Supplier" %}</option>
{% for supplier in suppliers %}
<option value="{{ supplier.id }}">{{ supplier.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Cost Price" %}</label>
<input type="number" step="0.001" name="cost_price" class="form-control rounded-3" value="0.000" required>
@ -373,7 +603,43 @@
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Stock" %}</label>
<input type="number" name="stock_quantity" class="form-control rounded-3" value="0" required>
<input type="number" name="stock_quantity" class="form-control rounded-3" step="0.01" value="0" required>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Stock Level (Alert)" %}</label>
<input type="number" name="min_stock_level" class="form-control rounded-3" step="0.01" value="0">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Opening Stock" %}</label>
<input type="number" name="opening_stock" class="form-control rounded-3" step="0.01" value="0">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">{% trans "VAT (%)" %}</label>
<input type="number" step="0.01" name="vat" class="form-control rounded-3" value="0.00">
</div>
<div class="col-md-4">
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" name="is_active" id="isActiveCheckAdd" checked>
<label class="form-check-label small fw-bold" for="isActiveCheckAdd">{% trans "Active" %}</label>
</div>
</div>
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Description" %}</label>
<textarea name="description" class="form-control rounded-3" rows="2"></textarea>
</div>
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Product Image" %}</label>
<input type="file" name="image" class="form-control rounded-3" accept="image/*">
</div>
<div class="col-md-4">
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" name="has_expiry" id="hasExpiryCheck">
<label class="form-check-label small fw-bold" for="hasExpiryCheck">{% trans "Has Expiry Date" %}</label>
</div>
</div>
<div class="col-md-4" id="expiryDateDiv" style="display: none;">
<label class="form-label small fw-bold">{% trans "Expiry Date" %}</label>
<input type="date" name="expiry_date" class="form-control rounded-3">
</div>
</div>
</div>
@ -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 @@
}
});
</script>
{% endblock %}
{% endblock %}

View File

@ -75,7 +75,7 @@
<input type="number" step="0.001" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.price" @input="calculateTotal">
</td>
<td>
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" step="0.01" v-model="item.quantity" @input="calculateTotal">
</td>
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]]</td>
<td class="text-end">

View File

@ -126,7 +126,7 @@
<div class="text-muted small">{{ item.product.name_en }}</div>
</td>
<td class="py-3 text-center">{{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }}</td>
<td class="py-3 text-center">{{ item.quantity }}</td>
<td class="py-3 text-center">{{ item.quantity|floatformat:2 }}</td>
<td class="py-3 text-end pe-4 fw-bold text-primary">{{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }}</td>
</tr>
{% endfor %}

View File

@ -80,7 +80,7 @@
<input type="number" step="0.001" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.price" @input="calculateTotal">
</td>
<td>
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" step="0.01" v-model="item.quantity" @input="calculateTotal">
</td>
<td class="text-end fw-bold">[[ currencySymbol ]][[ (parseFloat(item.price) * parseFloat(item.quantity)).toFixed(decimalPlaces) ]]</td>
<td class="text-end">

View File

@ -598,11 +598,11 @@
<div style="flex: 1;">
<div class="fw-bold small" dir="rtl">${item.name_ar}</div>
<div class="text-muted" style="font-size: 0.7rem;">${item.name_en}</div>
<div class="text-muted small">${currency} ${formatAmount(item.price)} x ${item.quantity}</div>
<div class="text-muted small">${currency} ${formatAmount(item.price)} x ${parseFloat(item.quantity).toFixed(2)}</div>
</div>
<div class="d-flex align-items-center gap-2">
<button class="btn btn-sm btn-outline-secondary rounded-circle" style="width:24px; height:24px; padding:0; display:flex; align-items:center; justify-content:center;" onclick="updateQuantity(${item.id}, -1)">-</button>
<span class="fw-bold small">${item.quantity}</span>
<span class="fw-bold small">${parseFloat(item.quantity).toFixed(2)}</span>
<button class="btn btn-sm btn-outline-secondary rounded-circle" style="width:24px; height:24px; padding:0; display:flex; align-items:center; justify-content:center;" onclick="updateQuantity(${item.id}, 1)">+</button>
</div>
</div>
@ -850,7 +850,7 @@
<div class="rtl">${item.name_ar}</div>
<div class="text-muted" style="font-size: 9px;">${item.name_en}</div>
</td>
<td style="text-align: center;">${item.qty}</td>
<td style="text-align: center;">${parseFloat(item.qty).toFixed(2)}</td>
<td style="text-align: right;">${data.business.currency} ${formatAmount(item.total)}</td>
</tr>
`;

View File

@ -60,6 +60,7 @@
<tr class="small text-uppercase text-muted fw-bold">
<th style="width: 40%;">{% trans "Product" %}</th>
<th class="text-center">{% trans "Cost Price" %}</th>
<th class="text-center">{% trans "Expiry Date" %}</th>
<th class="text-center" style="width: 15%;">{% trans "Quantity" %}</th>
<th class="text-end">{% trans "Total" %}</th>
<th></th>
@ -75,7 +76,10 @@
<input type="number" step="0.001" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.price" @input="calculateTotal">
</td>
<td>
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
<input type="date" class="form-control form-control-sm border-0 border-bottom rounded-0" v-model="item.expiry_date">
</td>
<td>
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" step="0.01" v-model="item.quantity" @input="calculateTotal">
</td>
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]]</td>
<td class="text-end">
@ -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,

View File

@ -120,7 +120,7 @@
<div class="text-muted small">{{ item.product.name_en }}</div>
</td>
<td class="py-3 text-center">{{ settings.currency_symbol }}{{ item.cost_price|floatformat:3 }}</td>
<td class="py-3 text-center">{{ item.quantity }}</td>
<td class="py-3 text-center">{{ item.quantity|floatformat:2 }}</td>
<td class="py-3 text-end pe-4 fw-bold text-primary">{{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }}</td>
</tr>
{% endfor %}

View File

@ -84,7 +84,7 @@
<input type="number" step="0.001" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.price" @input="calculateTotal">
</td>
<td>
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" step="0.01" v-model="item.quantity" @input="calculateTotal">
</td>
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]]</td>
<td class="text-end">

View File

@ -98,7 +98,7 @@
<div class="text-muted small">{{ item.product.name_en }}</div>
</td>
<td class="py-3 text-center">{{ settings.currency_symbol }}{{ item.cost_price|floatformat:3 }}</td>
<td class="py-3 text-center">{{ item.quantity }}</td>
<td class="py-3 text-center">{{ item.quantity|floatformat:2 }}</td>
<td class="py-3 text-end pe-4 fw-bold text-danger">{{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }}</td>
</tr>
{% endfor %}

View File

@ -75,7 +75,7 @@
<input type="number" step="0.001" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.price" @input="calculateTotal">
</td>
<td>
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" step="0.01" v-model="item.quantity" @input="calculateTotal">
</td>
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]]</td>
<td class="text-end">

View File

@ -142,7 +142,7 @@
<div class="text-muted small">{{ item.product.name_en }}</div>
</td>
<td class="py-3 text-center">{{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }}</td>
<td class="py-3 text-center">{{ item.quantity }}</td>
<td class="py-3 text-center">{{ item.quantity|floatformat:2 }}</td>
<td class="py-3 text-end pe-4 fw-bold text-primary">{{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }}</td>
</tr>
{% endfor %}

View File

@ -84,7 +84,7 @@
<input type="number" step="0.001" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.price" @input="calculateTotal">
</td>
<td>
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" step="0.01" v-model="item.quantity" @input="calculateTotal">
</td>
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]]</td>
<td class="text-end">

View File

@ -98,7 +98,7 @@
<div class="text-muted small">{{ item.product.name_en }}</div>
</td>
<td class="py-3 text-center">{{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }}</td>
<td class="py-3 text-center">{{ item.quantity }}</td>
<td class="py-3 text-center">{{ item.quantity|floatformat:2 }}</td>
<td class="py-3 text-end pe-4 fw-bold text-danger">{{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }}</td>
</tr>
{% endfor %}

View File

@ -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)

35
core/views_patch.py Normal file
View File

@ -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')