diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 87e4473..4700987 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index d38b33f..ada3ad1 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/templates/core/inventory.html b/core/templates/core/inventory.html index 17ac61d..3689b85 100644 --- a/core/templates/core/inventory.html +++ b/core/templates/core/inventory.html @@ -19,6 +19,9 @@ + @@ -475,8 +478,7 @@ - - + {% empty %} @@ -516,34 +518,54 @@
- +
+ + +
- +
+ + +
- +
+ + +
- +
+ + +
@@ -598,6 +620,131 @@
+ + + + + + + + + + + + + + {% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index a8f3132..4c768d2 100644 --- a/core/urls.py +++ b/core/urls.py @@ -59,20 +59,25 @@ urlpatterns = [ path('suppliers/add/', views.add_supplier, name='add_supplier'), path('suppliers/edit//', views.edit_supplier, name='edit_supplier'), path('suppliers/delete//', views.delete_supplier, name='delete_supplier'), + path('api/add-supplier-ajax/', views.add_supplier_ajax, name='add_supplier_ajax'), # Inventory + path('inventory/suggest-sku/', views.suggest_sku, name='suggest_sku'), path('inventory/add/', views.add_product, name='add_product'), path('inventory/edit//', views.edit_product, name='edit_product'), path('inventory/delete//', views.delete_product, name='delete_product'), path('inventory/barcodes/', views.barcode_labels, name='barcode_labels'), + path('inventory/import/', views.import_products, name='import_products'), # Categories path('inventory/category/add/', views.add_category, name='add_category'), path('inventory/category/edit//', views.edit_category, name='edit_category'), path('inventory/category/delete//', views.delete_category, name='delete_category'), + path('api/add-category-ajax/', views.add_category_ajax, name='add_category_ajax'), # Units path('inventory/unit/add/', views.add_unit, name='add_unit'), path('inventory/unit/edit//', views.edit_unit, name='edit_unit'), path('inventory/unit/delete//', views.delete_unit, name='delete_unit'), -] + path('api/add-unit-ajax/', views.add_unit_ajax, name='add_unit_ajax'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index ca90f7f..1617e4c 100644 --- a/core/views.py +++ b/core/views.py @@ -1,3 +1,5 @@ +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.functions import TruncDate, TruncMonth @@ -15,6 +17,7 @@ from datetime import timedelta from django.utils import timezone from django.contrib import messages from django.utils.text import slugify +import openpyxl def index(request): """ @@ -709,6 +712,17 @@ def delete_supplier(request, pk): messages.success(request, "Supplier deleted successfully!") 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): if request.method == 'POST': name_en = request.POST.get('name_en') @@ -717,6 +731,11 @@ def add_product(request): unit_id = request.POST.get('unit') supplier_id = request.POST.get('supplier') 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) price = request.POST.get('price', 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') context = {'products': products} 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)