enhancing items form
This commit is contained in:
parent
e473add476
commit
3f9709efef
Binary file not shown.
Binary file not shown.
35
core/edit_product_fixed.py
Normal file
35
core/edit_product_fixed.py
Normal 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')
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0021_product_min_stock_level.py
Normal file
18
core/migrations/0021_product_min_stock_level.py
Normal 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)'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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()
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
`;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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
35
core/views_patch.py
Normal 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')
|
||||
Loading…
x
Reference in New Issue
Block a user