Autosave: 20260202-103700

This commit is contained in:
Flatlogic Bot 2026-02-02 10:37:01 +00:00
parent f80934e391
commit 1e0d4f6540
5 changed files with 430 additions and 22 deletions

View File

@ -19,6 +19,9 @@
<button class="btn btn-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addProductModal"> <button class="btn btn-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addProductModal">
<i class="bi bi-plus-lg me-2"></i>{% trans "Add Item" %} <i class="bi bi-plus-lg me-2"></i>{% trans "Add Item" %}
</button> </button>
<button class="btn btn-success shadow-sm" data-bs-toggle="modal" data-bs-target="#importModal">
<i class="bi bi-file-earmark-excel me-2"></i>{% trans "Import" %}
</button>
<button class="btn btn-outline-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addCategoryModal"> <button class="btn btn-outline-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addCategoryModal">
<i class="bi bi-folder-plus me-2"></i>{% trans "Add Category" %} <i class="bi bi-folder-plus me-2"></i>{% trans "Add Category" %}
</button> </button>
@ -475,8 +478,7 @@
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button> <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 "Update Unit" %}</button> <button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Update Unit" %}</button>
</div> </div>
</form> </form> </div>
</div>
</div> </div>
</div> </div>
{% empty %} {% empty %}
@ -516,34 +518,54 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Barcode / SKU" %}</label> <label class="form-label small fw-bold">{% trans "Barcode / SKU" %}</label>
<input type="text" name="sku" class="form-control rounded-3" required> <div class="input-group">
<input type="text" name="sku" id="skuInput" class="form-control rounded-3" placeholder="{% trans 'Auto-generated if empty' %}">
<button class="btn btn-outline-primary" type="button" id="suggestSkuBtn" title="{% trans 'Suggest SKU' %}">
<i class="bi bi-magic"></i>
</button>
</div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Category" %}</label> <label class="form-label small fw-bold">{% trans "Category" %}</label>
<select name="category" class="form-select rounded-3" required> <div class="input-group">
<option value="">{% trans "Select Category" %}</option> <select name="category" id="categorySelect" class="form-select rounded-3" required>
{% for category in categories %} <option value="">{% trans "Select Category" %}</option>
<option value="{{ category.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ category.name_ar }}{% else %}{{ category.name_en }}{% endif %}</option> {% for category in categories %}
{% endfor %} <option value="{{ category.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ category.name_ar }}{% else %}{{ category.name_en }}{% endif %}</option>
</select> {% endfor %}
</select>
<button class="btn btn-outline-primary" type="button" data-bs-toggle="modal" data-bs-target="#quickAddCategoryModal">
<i class="bi bi-plus-lg"></i>
</button>
</div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label small fw-bold">{% trans "Unit" %}</label> <label class="form-label small fw-bold">{% trans "Unit" %}</label>
<select name="unit" class="form-select rounded-3"> <div class="input-group">
<option value="">{% trans "Select Unit" %}</option> <select name="unit" id="unitSelect" class="form-select rounded-3">
{% for unit in units %} <option value="">{% trans "Select Unit" %}</option>
<option value="{{ unit.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ unit.name_ar }}{% else %}{{ unit.name_en }}{% endif %}</option> {% for unit in units %}
{% endfor %} <option value="{{ unit.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ unit.name_ar }}{% else %}{{ unit.name_en }}{% endif %}</option>
</select> {% endfor %}
</select>
<button class="btn btn-outline-primary" type="button" data-bs-toggle="modal" data-bs-target="#quickAddUnitModal">
<i class="bi bi-plus-lg"></i>
</button>
</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Supplier" %}</label> <label class="form-label small fw-bold">{% trans "Supplier" %}</label>
<select name="supplier" class="form-select rounded-3"> <div class="input-group">
<option value="">{% trans "Select Supplier" %}</option> <select name="supplier" id="supplierSelect" class="form-select rounded-3">
{% for supplier in suppliers %} <option value="">{% trans "Select Supplier" %}</option>
<option value="{{ supplier.id }}">{{ supplier.name }}</option> {% for supplier in suppliers %}
{% endfor %} <option value="{{ supplier.id }}">{{ supplier.name }}</option>
</select> {% endfor %}
</select>
<button class="btn btn-outline-primary" type="button" data-bs-toggle="modal" data-bs-target="#quickAddSupplierModal">
<i class="bi bi-plus-lg"></i>
</button>
</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Product Picture" %}</label> <label class="form-label small fw-bold">{% trans "Product Picture" %}</label>
@ -598,6 +620,131 @@
</div> </div>
</div> </div>
<!-- Quick Add Category Modal -->
<div class="modal fade" id="quickAddCategoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Quick Add Category" %}</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-12">
<label class="form-label small fw-bold">{% trans "Name (English)" %}</label>
<input type="text" id="quickCatNameEn" class="form-control rounded-3" required>
</div>
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Name (Arabic)" %}</label>
<input type="text" id="quickCatNameAr" class="form-control rounded-3" dir="rtl" required>
</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="button" class="btn btn-primary rounded-3 px-4" id="saveQuickCategory">{% trans "Save" %}</button>
</div>
</div>
</div>
</div>
<!-- Quick Add Unit Modal -->
<div class="modal fade" id="quickAddUnitModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Quick Add Unit" %}</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-12">
<label class="form-label small fw-bold">{% trans "Name (English)" %}</label>
<input type="text" id="quickUnitNameEn" class="form-control rounded-3" required>
</div>
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Name (Arabic)" %}</label>
<input type="text" id="quickUnitNameAr" class="form-control rounded-3" dir="rtl" required>
</div>
<div class="col-12">
<label class="form-label small fw-bold">{% trans "Short Name" %}</label>
<input type="text" id="quickUnitShortName" class="form-control rounded-3" required>
</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="button" class="btn btn-primary rounded-3 px-4" id="saveQuickUnit">{% trans "Save" %}</button>
</div>
</div>
</div>
</div>
<!-- Quick Add Supplier Modal -->
<div class="modal fade" id="quickAddSupplierModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Quick Add Supplier" %}</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-12">
<label class="form-label small fw-bold">{% trans "Supplier Name" %}</label>
<input type="text" id="quickSuppName" class="form-control rounded-3" required>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Contact Person" %}</label>
<input type="text" id="quickSuppContact" class="form-control rounded-3">
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Phone" %}</label>
<input type="text" id="quickSuppPhone" class="form-control rounded-3">
</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="button" class="btn btn-primary rounded-3 px-4" id="saveQuickSupplier">{% trans "Save" %}</button>
</div>
</div>
</div>
</div>
<!-- Import Modal -->
<div class="modal fade" id="importModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Import Items from Excel" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{% url 'import_products' %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div class="modal-body">
<div class="alert alert-info rounded-3 small">
<i class="bi bi-info-circle me-2"></i>
{% trans "Excel file should have these columns in order:" %}<br>
<strong>{% trans "Name (Eng), Name (Ar), SKU, Cost Price, Sale Price" %}</strong>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Select Excel File (.xlsx)" %}</label>
<input type="file" name="excel_file" class="form-control rounded-3" accept=".xlsx" required>
</div>
<p class="text-muted small">
{% trans "Note: If SKU exists, the item will be updated. Otherwise, a new item will be created in the 'General' category." %}
</p>
</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-success rounded-3 px-4">{% trans "Start Import" %}</button>
</div>
</form>
</div>
</div>
</div>
<!-- Add Category Modal --> <!-- Add Category Modal -->
<div class="modal fade" id="addCategoryModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="addCategoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
@ -663,4 +810,95 @@
</div> </div>
</div> </div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Suggest SKU
const suggestBtn = document.getElementById('suggestSkuBtn');
if (suggestBtn) {
suggestBtn.addEventListener('click', function() {
fetch('{% url "suggest_sku" %}')
.then(response => response.json())
.then(data => {
document.getElementById('skuInput').value = data.sku;
})
.catch(error => console.error('Error fetching SKU:', error));
});
}
// Quick Add Category
document.getElementById('saveQuickCategory').addEventListener('click', function() {
const nameEn = document.getElementById('quickCatNameEn').value;
const nameAr = document.getElementById('quickCatNameAr').value;
if (!nameEn || !nameAr) return alert('Please fill all fields');
fetch('{% url "add_category_ajax" %}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name_en: nameEn, name_ar: nameAr })
})
.then(response => response.json())
.then(data => {
if (data.success) {
const select = document.getElementById('categorySelect');
const option = new Option(data.name_en + ' | ' + data.name_ar, data.id, true, true);
select.add(option);
bootstrap.Modal.getInstance(document.getElementById('quickAddCategoryModal')).hide();
} else {
alert('Error: ' + data.error);
}
});
});
// Quick Add Unit
document.getElementById('saveQuickUnit').addEventListener('click', function() {
const nameEn = document.getElementById('quickUnitNameEn').value;
const nameAr = document.getElementById('quickUnitNameAr').value;
const shortName = document.getElementById('quickUnitShortName').value;
if (!nameEn || !nameAr || !shortName) return alert('Please fill all fields');
fetch('{% url "add_unit_ajax" %}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name_en: nameEn, name_ar: nameAr, short_name: shortName })
})
.then(response => response.json())
.then(data => {
if (data.success) {
const select = document.getElementById('unitSelect');
const option = new Option(data.name_en + ' | ' + data.name_ar, data.id, true, true);
select.add(option);
bootstrap.Modal.getInstance(document.getElementById('quickAddUnitModal')).hide();
} else {
alert('Error: ' + data.error);
}
});
});
// Quick Add Supplier
document.getElementById('saveQuickSupplier').addEventListener('click', function() {
const name = document.getElementById('quickSuppName').value;
const contact = document.getElementById('quickSuppContact').value;
const phone = document.getElementById('quickSuppPhone').value;
if (!name) return alert('Please fill supplier name');
fetch('{% url "add_supplier_ajax" %}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, contact_person: contact, phone: phone })
})
.then(response => response.json())
.then(data => {
if (data.success) {
const select = document.getElementById('supplierSelect');
const option = new Option(data.name, data.id, true, true);
select.add(option);
bootstrap.Modal.getInstance(document.getElementById('quickAddSupplierModal')).hide();
} else {
alert('Error: ' + data.error);
}
});
});
});
</script>
{% endblock %} {% endblock %}

View File

@ -59,20 +59,25 @@ urlpatterns = [
path('suppliers/add/', views.add_supplier, name='add_supplier'), path('suppliers/add/', views.add_supplier, name='add_supplier'),
path('suppliers/edit/<int:pk>/', views.edit_supplier, name='edit_supplier'), path('suppliers/edit/<int:pk>/', views.edit_supplier, name='edit_supplier'),
path('suppliers/delete/<int:pk>/', views.delete_supplier, name='delete_supplier'), path('suppliers/delete/<int:pk>/', views.delete_supplier, name='delete_supplier'),
path('api/add-supplier-ajax/', views.add_supplier_ajax, name='add_supplier_ajax'),
# Inventory # Inventory
path('inventory/suggest-sku/', views.suggest_sku, name='suggest_sku'),
path('inventory/add/', views.add_product, name='add_product'), path('inventory/add/', views.add_product, name='add_product'),
path('inventory/edit/<int:pk>/', views.edit_product, name='edit_product'), path('inventory/edit/<int:pk>/', views.edit_product, name='edit_product'),
path('inventory/delete/<int:pk>/', views.delete_product, name='delete_product'), path('inventory/delete/<int:pk>/', views.delete_product, name='delete_product'),
path('inventory/barcodes/', views.barcode_labels, name='barcode_labels'), path('inventory/barcodes/', views.barcode_labels, name='barcode_labels'),
path('inventory/import/', views.import_products, name='import_products'),
# Categories # Categories
path('inventory/category/add/', views.add_category, name='add_category'), path('inventory/category/add/', views.add_category, name='add_category'),
path('inventory/category/edit/<int:pk>/', views.edit_category, name='edit_category'), path('inventory/category/edit/<int:pk>/', views.edit_category, name='edit_category'),
path('inventory/category/delete/<int:pk>/', views.delete_category, name='delete_category'), path('inventory/category/delete/<int:pk>/', views.delete_category, name='delete_category'),
path('api/add-category-ajax/', views.add_category_ajax, name='add_category_ajax'),
# Units # Units
path('inventory/unit/add/', views.add_unit, name='add_unit'), path('inventory/unit/add/', views.add_unit, name='add_unit'),
path('inventory/unit/edit/<int:pk>/', views.edit_unit, name='edit_unit'), path('inventory/unit/edit/<int:pk>/', views.edit_unit, name='edit_unit'),
path('inventory/unit/delete/<int:pk>/', views.delete_unit, name='delete_unit'), path('inventory/unit/delete/<int:pk>/', views.delete_unit, name='delete_unit'),
] path('api/add-unit-ajax/', views.add_unit_ajax, name='add_unit_ajax'),
]

View File

@ -1,3 +1,5 @@
import random
import string
from django.shortcuts import render, get_object_or_404, redirect 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
from django.db.models.functions import TruncDate, TruncMonth from django.db.models.functions import TruncDate, TruncMonth
@ -15,6 +17,7 @@ from datetime import timedelta
from django.utils import timezone from django.utils import timezone
from django.contrib import messages from django.contrib import messages
from django.utils.text import slugify from django.utils.text import slugify
import openpyxl
def index(request): def index(request):
""" """
@ -709,6 +712,17 @@ def delete_supplier(request, pk):
messages.success(request, "Supplier deleted successfully!") messages.success(request, "Supplier deleted successfully!")
return redirect('suppliers') return redirect('suppliers')
def suggest_sku(request):
"""
API endpoint to suggest a unique SKU.
"""
while True:
# Generate a random 8-digit number
sku = "".join(random.choices(string.digits, k=8))
if not Product.objects.filter(sku=sku).exists():
return JsonResponse({"sku": sku})
def add_product(request): def add_product(request):
if request.method == 'POST': if request.method == 'POST':
name_en = request.POST.get('name_en') name_en = request.POST.get('name_en')
@ -717,6 +731,11 @@ def add_product(request):
unit_id = request.POST.get('unit') unit_id = request.POST.get('unit')
supplier_id = request.POST.get('supplier') supplier_id = request.POST.get('supplier')
sku = request.POST.get('sku') sku = request.POST.get('sku')
if not sku:
while True:
sku = ''.join(random.choices(string.digits, k=8))
if not Product.objects.filter(sku=sku).exists():
break
cost_price = request.POST.get('cost_price', 0) cost_price = request.POST.get('cost_price', 0)
price = request.POST.get('price', 0) price = request.POST.get('price', 0)
vat = request.POST.get('vat', 0) vat = request.POST.get('vat', 0)
@ -839,3 +858,149 @@ def barcode_labels(request):
products = Product.objects.filter(is_active=True).order_by('name_en') products = Product.objects.filter(is_active=True).order_by('name_en')
context = {'products': products} context = {'products': products}
return render(request, 'core/barcode_labels.html', context) return render(request, 'core/barcode_labels.html', context)
def import_products(request):
"""
Import products from an Excel (.xlsx) file.
Expected columns: Name (Eng), Name (Ar), SKU, Cost Price, Sale Price
"""
if request.method == 'POST' and request.FILES.get('excel_file'):
excel_file = request.FILES['excel_file']
if not excel_file.name.endswith('.xlsx'):
messages.error(request, "Please upload a valid .xlsx file.")
return redirect('inventory')
try:
wb = openpyxl.load_workbook(excel_file)
sheet = wb.active
# Get or create a default category
default_category, _ = Category.objects.get_or_create(
name_en="General",
defaults={'name_ar': "عام", 'slug': 'general'}
)
count = 0
updated_count = 0
errors = []
# Skip header row (min_row=2)
for i, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2):
if not any(row): continue # Skip empty rows
# Unpack columns with fallbacks for safety
# Format: name_en, name_ar, sku, cost_price, sale_price
name_en = str(row[0]).strip() if row[0] else None
name_ar = str(row[1]).strip() if len(row) > 1 and row[1] else name_en
sku = str(row[2]).strip() if len(row) > 2 and row[2] else None
cost_price = row[3] if len(row) > 3 and row[3] is not None else 0
sale_price = row[4] if len(row) > 4 and row[4] is not None else 0
if not name_en:
errors.append(f"Row {i}: Missing English Name. Skipped.")
continue
if not sku:
# Generate unique SKU if missing
while True:
sku = "".join(random.choices(string.digits, k=8))
if not Product.objects.filter(sku=sku).exists():
break
product, created = Product.objects.update_or_create(
sku=sku,
defaults={
'name_en': name_en,
'name_ar': name_ar,
'cost_price': cost_price,
'price': sale_price,
'category': default_category,
'is_active': True
}
)
if created:
count += 1
else:
updated_count += 1
if count > 0 or updated_count > 0:
msg = f"Import completed: {count} new items added"
if updated_count > 0:
msg += f", {updated_count} items updated"
messages.success(request, msg)
if errors:
for error in errors:
messages.warning(request, error)
except Exception as e:
messages.error(request, f"Error processing file: {str(e)}")
return redirect('inventory')
@csrf_exempt
def add_category_ajax(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
name_en = data.get('name_en')
name_ar = data.get('name_ar')
if not name_en or not name_ar:
return JsonResponse({'success': False, 'error': 'Missing names'}, status=400)
slug = slugify(name_en)
category = Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug)
return JsonResponse({
'success': True,
'id': category.id,
'name_en': category.name_en,
'name_ar': category.name_ar
})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@csrf_exempt
def add_unit_ajax(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
name_en = data.get('name_en')
name_ar = data.get('name_ar')
short_name = data.get('short_name')
if not name_en or not name_ar or not short_name:
return JsonResponse({'success': False, 'error': 'Missing fields'}, status=400)
unit = Unit.objects.create(name_en=name_en, name_ar=name_ar, short_name=short_name)
return JsonResponse({
'success': True,
'id': unit.id,
'name_en': unit.name_en,
'name_ar': unit.name_ar
})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@csrf_exempt
def add_supplier_ajax(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
name = data.get('name')
contact_person = data.get('contact_person', '')
phone = data.get('phone', '')
if not name:
return JsonResponse({'success': False, 'error': 'Missing name'}, status=400)
supplier = Supplier.objects.create(name=name, contact_person=contact_person, phone=phone)
return JsonResponse({
'success': True,
'id': supplier.id,
'name': supplier.name
})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)