import json import decimal import logging from django.shortcuts import render, redirect, get_object_or_404 from django.http import JsonResponse, HttpResponse from django.contrib.auth.decorators import login_required from django.views.decorators.csrf import csrf_exempt from django.utils import timezone from django.contrib import messages from django.db import transaction from django.db.models import Sum, Q, Count, F from django.db.models.functions import TruncMonth, TruncDay from django.core.paginator import Paginator from django.urls import reverse from django.utils.text import slugify from .models import ( Product, Category, Unit, Supplier, Customer, Sale, SaleItem, Purchase, PurchaseItem, Expense, ExpenseCategory, SalePayment, PurchasePayment, SystemSetting, CashierSession, SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem, HeldSale, Quotation, QuotationItem, PaymentMethod, LoyaltyTier, Device, CashierCounterRegistry, UserProfile, PurchaseOrder, PurchaseOrderItem ) from .forms import SystemSettingForm, CustomerForm, SupplierForm, ProductForm, CategoryForm, UnitForm, ExpenseForm from .helpers import number_to_words_en logger = logging.getLogger(__name__) # --- Dashboard --- @login_required def index(request): settings = SystemSetting.objects.first() today = timezone.now().date() seven_days_ago = today - timezone.timedelta(days=7) this_year = today.year # 1. Financial Headlines total_sales_amount = Sale.objects.aggregate(t=Sum('total_amount'))['t'] or 0 total_receivables = Sale.objects.filter(status__in=['partial', 'unpaid']).aggregate(t=Sum('balance_due'))['t'] or 0 # For payables, we sum the balance_due of Purchases total_payables = Purchase.objects.filter(status__in=['partial', 'unpaid']).aggregate(t=Sum('balance_due'))['t'] or 0 # 2. Counts total_sales_count = Sale.objects.count() total_products = Product.objects.filter(is_active=True).count() total_customers = Customer.objects.count() # 3. Monthly Sales Chart (This Year) monthly_sales = Sale.objects.filter(created_at__year=this_year)\ .annotate(month=TruncMonth('created_at'))\ .values('month')\ .annotate(total=Sum('total_amount'))\ .order_by('month') monthly_labels = [] monthly_data = [] # Initialize all months to 0 months_map = {i: 0 for i in range(1, 13)} for entry in monthly_sales: months_map[entry['month'].month] = float(entry['total']) import calendar for i in range(1, 13): monthly_labels.append(calendar.month_name[i]) monthly_data.append(months_map[i]) # 4. Daily Sales Chart (Last 7 Days) daily_sales = Sale.objects.filter(created_at__date__gte=seven_days_ago)\ .annotate(day=TruncDay('created_at'))\ .values('day')\ .annotate(total=Sum('total_amount'))\ .order_by('day') daily_map = {} for entry in daily_sales: daily_map[entry['day'].date()] = float(entry['total']) chart_labels = [] chart_data = [] for i in range(7): day = seven_days_ago + timezone.timedelta(days=i) chart_labels.append(day.strftime('%a %d')) chart_data.append(daily_map.get(day, 0)) # 5. Sales by Category category_sales = SaleItem.objects.values('product__category__name_en')\ .annotate(total=Sum('line_total'))\ .order_by('-total')[:6] category_labels = [item['product__category__name_en'] for item in category_sales] category_data = [float(item['total']) for item in category_sales] # 6. Payment Methods payment_stats = SalePayment.objects.values('payment_method__name_en')\ .annotate(total=Sum('amount'))\ .order_by('-total') payment_labels = [item['payment_method__name_en'] or 'Cash' for item in payment_stats] payment_data = [float(item['total']) for item in payment_stats] # 7. Top Selling Products top_products = SaleItem.objects.values('product__name_en', 'product__name_ar')\ .annotate(total_qty=Sum('quantity'), total_rev=Sum('line_total'))\ .order_by('-total_qty')[:5] # 8. Low Stock & Expiry low_stock_products = Product.objects.filter(stock_quantity__lte=F('min_stock_level'), is_active=True)[:5] low_stock_count = Product.objects.filter(stock_quantity__lte=F('min_stock_level'), is_active=True).count() expired_count = Product.objects.filter(has_expiry=True, expiry_date__lt=today).count() # 9. Recent Sales recent_sales = Sale.objects.select_related('customer', 'created_by').order_by('-created_at')[:5] context = { 'site_settings': settings, 'total_sales_amount': total_sales_amount, 'total_receivables': total_receivables, 'total_payables': total_payables, 'total_sales_count': total_sales_count, 'total_products': total_products, 'total_customers': total_customers, 'monthly_labels': monthly_labels, 'monthly_data': monthly_data, 'chart_labels': chart_labels, 'chart_data': chart_data, 'category_labels': category_labels, 'category_data': category_data, 'payment_labels': payment_labels, 'payment_data': payment_data, 'top_products': top_products, 'low_stock_products': low_stock_products, 'low_stock_count': low_stock_count, 'expired_count': expired_count, 'recent_sales': recent_sales, } return render(request, 'core/index.html', context) # --- Inventory --- @login_required def inventory(request): products = Product.objects.all().select_related('category', 'unit', 'supplier').order_by('-created_at') # Filtering category_id = request.GET.get('category') if category_id: products = products.filter(category_id=category_id) query = request.GET.get('q') if query: products = products.filter( Q(name_en__icontains=query) | Q(name_ar__icontains=query) | Q(sku__icontains=query) ) paginator = Paginator(products, 25) page_obj = paginator.get_page(request.GET.get('page')) context = { 'products': page_obj, 'categories': Category.objects.all(), 'units': Unit.objects.all(), 'suppliers': Supplier.objects.all(), 'expired_products': Product.objects.filter(has_expiry=True, expiry_date__lt=timezone.now().date()), 'expiring_soon_products': Product.objects.filter( has_expiry=True, expiry_date__range=[timezone.now().date(), timezone.now().date() + timezone.timedelta(days=30)] ) } return render(request, 'core/inventory.html', context) # --- Category & Unit Management --- @csrf_exempt @login_required def add_category_ajax(request): if request.method != 'POST': return JsonResponse({'success': False, 'error': 'Invalid method'}) try: try: data = json.loads(request.body) name_en = data.get('name_en') name_ar = data.get('name_ar') except (json.JSONDecodeError, TypeError): name_en = request.POST.get('name_en') name_ar = request.POST.get('name_ar') if not name_en or not name_ar: return JsonResponse({'success': False, 'error': 'Names are required'}) base_slug = slugify(name_en) if not base_slug: base_slug = f"cat-{timezone.now().strftime('%Y%m%d%H%M%S')}" slug = base_slug counter = 1 while Category.objects.filter(slug=slug).exists(): slug = f"{base_slug}-{counter}" counter += 1 Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug) return JsonResponse({'success': True}) except Exception as e: logger.error(f"Error adding category: {e}") return JsonResponse({'success': False, 'error': str(e)}) @csrf_exempt @login_required def add_unit_ajax(request): if request.method != 'POST': return JsonResponse({'success': False, 'error': 'Invalid method'}) try: try: data = json.loads(request.body) name_en = data.get('name_en') name_ar = data.get('name_ar') short_name = data.get('short_name') except (json.JSONDecodeError, TypeError): name_en = request.POST.get('name_en') name_ar = request.POST.get('name_ar') short_name = request.POST.get('short_name') if not name_en or not name_ar or not short_name: return JsonResponse({'success': False, 'error': 'All fields are required'}) Unit.objects.create(name_en=name_en, name_ar=name_ar, short_name=short_name) return JsonResponse({'success': True}) except Exception as e: logger.error(f"Error adding unit: {e}") return JsonResponse({'success': False, 'error': str(e)}) @login_required def add_category(request): # Fallback for non-AJAX if request.method == 'POST': name_en = request.POST.get('name_en') name_ar = request.POST.get('name_ar') if name_en and name_ar: try: base_slug = slugify(name_en) or f"cat-{timezone.now().timestamp()}" slug = base_slug counter = 1 while Category.objects.filter(slug=slug).exists(): slug = f"{base_slug}-{counter}" counter += 1 Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug) messages.success(request, "Category added!") except Exception as e: messages.error(request, f"Error: {e}") return redirect('inventory') @login_required def edit_category(request, pk): cat = get_object_or_404(Category, pk=pk) if request.method == 'POST': cat.name_en = request.POST.get('name_en') cat.name_ar = request.POST.get('name_ar') cat.save() messages.success(request, "Category updated!") return redirect('inventory') @login_required def delete_category(request, pk): get_object_or_404(Category, pk=pk).delete() messages.success(request, "Category deleted!") return redirect('inventory') @login_required def add_unit(request): if request.method == 'POST': try: Unit.objects.create( name_en=request.POST.get('name_en'), name_ar=request.POST.get('name_ar'), short_name=request.POST.get('short_name') ) messages.success(request, "Unit added!") except Exception as e: messages.error(request, f"Error: {e}") return redirect('inventory') @login_required def edit_unit(request, pk): unit = get_object_or_404(Unit, pk=pk) if request.method == 'POST': unit.name_en = request.POST.get('name_en') unit.name_ar = request.POST.get('name_ar') unit.short_name = request.POST.get('short_name') unit.save() messages.success(request, "Unit updated!") return redirect('inventory') @login_required def delete_unit(request, pk): get_object_or_404(Unit, pk=pk).delete() messages.success(request, "Unit deleted!") return redirect('inventory') # --- Product Management --- @login_required def add_product(request): if request.method == 'POST': try: p = Product() p.name_en = request.POST.get('name_en') p.name_ar = request.POST.get('name_ar') p.sku = request.POST.get('sku') p.category_id = request.POST.get('category') p.unit_id = request.POST.get('unit') or None p.supplier_id = request.POST.get('supplier') or None p.cost_price = request.POST.get('cost_price') or 0 p.price = request.POST.get('price') or 0 p.stock_quantity = request.POST.get('stock_quantity') or 0 p.min_stock_level = request.POST.get('min_stock_level') or 0 p.vat = request.POST.get('vat') or 0 p.description = request.POST.get('description', '') p.is_active = request.POST.get('is_active') == 'on' p.has_expiry = request.POST.get('has_expiry') == 'on' if p.has_expiry: p.expiry_date = request.POST.get('expiry_date') if 'image' in request.FILES: p.image = request.FILES['image'] p.save() messages.success(request, "Product added!") except Exception as e: messages.error(request, f"Error adding product: {e}") return redirect('inventory') @login_required def edit_product(request, pk): p = get_object_or_404(Product, pk=pk) if request.method == 'POST': p.name_en = request.POST.get('name_en') p.name_ar = request.POST.get('name_ar') p.sku = request.POST.get('sku') p.category_id = request.POST.get('category') p.unit_id = request.POST.get('unit') or None p.supplier_id = request.POST.get('supplier') or None p.cost_price = request.POST.get('cost_price') or 0 p.price = request.POST.get('price') or 0 p.stock_quantity = request.POST.get('stock_quantity') or 0 p.min_stock_level = request.POST.get('min_stock_level') or 0 p.vat = request.POST.get('vat') or 0 p.description = request.POST.get('description', '') p.is_active = request.POST.get('is_active') == 'on' p.has_expiry = request.POST.get('has_expiry') == 'on' if p.has_expiry: p.expiry_date = request.POST.get('expiry_date') else: p.expiry_date = None if 'image' in request.FILES: p.image = request.FILES['image'] p.save() messages.success(request, "Product updated!") return redirect('inventory') @login_required def delete_product(request, pk): get_object_or_404(Product, pk=pk).delete() messages.success(request, "Product deleted!") return redirect('inventory') # --- POS --- @login_required def pos(request): settings = SystemSetting.objects.first() products = Product.objects.filter(is_active=True).select_related('category') if not settings or not settings.allow_zero_stock_sales: products = products.filter(Q(stock_quantity__gt=0) | Q(is_service=True)) context = { 'products': products, 'categories': Category.objects.all(), 'customers': Customer.objects.all(), 'payment_methods': PaymentMethod.objects.filter(is_active=True), 'active_session': CashierSession.objects.filter(user=request.user, status='active').first(), 'settings': settings } return render(request, 'core/pos.html', context) @csrf_exempt @login_required def create_sale_api(request): if request.method != 'POST': return JsonResponse({'success': False, 'error': 'Method not allowed'}) try: data = json.loads(request.body) items = data.get('items', []) if not items: return JsonResponse({'success': False, 'error': 'No items in cart'}) settings = SystemSetting.objects.first() allow_zero_stock = settings.allow_zero_stock_sales if settings else False with transaction.atomic(): sale = Sale.objects.create( customer_id=data.get('customer_id') or None, payment_type=data.get('payment_type', 'cash'), discount=data.get('discount') or 0, notes=data.get('notes', ''), created_by=request.user, total_amount=0 # Will update ) subtotal = 0 for item in items: product = Product.objects.get(pk=item['id']) qty = decimal.Decimal(str(item['quantity'])) price = decimal.Decimal(str(item['price'])) if not product.is_service and not allow_zero_stock: if product.stock_quantity < qty: raise Exception(f"Insufficient stock for {product.name_en}") if not product.is_service: product.stock_quantity -= qty product.save() line_total = qty * price subtotal += line_total SaleItem.objects.create( sale=sale, product=product, quantity=qty, unit_price=price, line_total=line_total ) sale.subtotal = subtotal sale.total_amount = subtotal - decimal.Decimal(str(sale.discount)) sale.paid_amount = sale.total_amount # POS full payment sale.save() SalePayment.objects.create( sale=sale, amount=sale.paid_amount, payment_method_id=data.get('payment_method_id'), created_by=request.user ) return JsonResponse({'success': True, 'sale_id': sale.id}) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}) # --- Sales & Reports --- @login_required def invoice_list(request): sales = Sale.objects.all().order_by('-created_at') paginator = Paginator(sales, 25) return render(request, 'core/invoices.html', { 'sales': paginator.get_page(request.GET.get('page')), 'customers': Customer.objects.all(), 'site_settings': SystemSetting.objects.first(), 'payment_methods': PaymentMethod.objects.filter(is_active=True) }) @login_required def settings_view(request): settings = SystemSetting.objects.first() if not settings: settings = SystemSetting.objects.create() if request.method == 'POST': form = SystemSettingForm(request.POST, request.FILES, instance=settings) if form.is_valid(): s = form.save(commit=False) if not s.wablas_server_url: s.wablas_server_url = '' if not s.wablas_secret_key: s.wablas_secret_key = '' if not s.wablas_token: s.wablas_token = '' s.save() messages.success(request, "Settings updated") else: form = SystemSettingForm(instance=settings) context = { 'form': form, 'settings': settings, 'devices': Device.objects.all(), 'loyalty_tiers': LoyaltyTier.objects.all() } return render(request, 'core/settings.html', context) # --- Stubs & Helpers --- @login_required def suggest_sku(request): return JsonResponse({'sku': 'SKU-' + timezone.now().strftime("%Y%m%d%H%M%S")}) @login_required def barcode_labels(request): products = Product.objects.filter(is_active=True).order_by('name_en') return render(request, 'core/barcode_labels.html', {'products': products}) # --- Customers --- @login_required def customers(request): customers = Customer.objects.all().order_by('name') paginator = Paginator(customers, 25) return render(request, 'core/customers.html', { 'customers': paginator.get_page(request.GET.get('page')) }) @csrf_exempt @login_required def add_customer_ajax(request): if request.method != 'POST': return JsonResponse({'success': False, 'error': 'Invalid method'}) try: try: data = json.loads(request.body) name = data.get('name') phone = data.get('phone') except: name = request.POST.get('name') phone = request.POST.get('phone') if not name: return JsonResponse({'success': False, 'error': 'Name is required'}) Customer.objects.create(name=name, phone=phone or '') return JsonResponse({'success': True}) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}) @login_required def add_customer(request): if request.method == 'POST': form = CustomerForm(request.POST) if form.is_valid(): form.save() messages.success(request, "Customer added") else: messages.error(request, "Error adding customer") return redirect('customers') @login_required def edit_customer(request, pk): c = get_object_or_404(Customer, pk=pk) if request.method == 'POST': c.name = request.POST.get('name') c.phone = request.POST.get('phone') c.email = request.POST.get('email') c.address = request.POST.get('address') c.save() messages.success(request, "Customer updated") return redirect('customers') @login_required def delete_customer(request, pk): get_object_or_404(Customer, pk=pk).delete() messages.success(request, "Customer deleted") return redirect('customers') # --- Suppliers --- @login_required def suppliers(request): suppliers = Supplier.objects.all().order_by('name') paginator = Paginator(suppliers, 25) return render(request, 'core/suppliers.html', { 'suppliers': paginator.get_page(request.GET.get('page')), 'site_settings': SystemSetting.objects.first() }) @csrf_exempt @login_required def add_supplier_ajax(request): if request.method != 'POST': return JsonResponse({'success': False, 'error': 'Invalid method'}) try: try: data = json.loads(request.body) name = data.get('name') contact_person = data.get('contact_person') phone = data.get('phone') except: name = request.POST.get('name') contact_person = request.POST.get('contact_person') phone = request.POST.get('phone') if not name: return JsonResponse({'success': False, 'error': 'Name is required'}) Supplier.objects.create(name=name, contact_person=contact_person or '', phone=phone or '') return JsonResponse({'success': True}) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}) @login_required def add_supplier(request): if request.method == 'POST': form = SupplierForm(request.POST) if form.is_valid(): form.save() messages.success(request, "Supplier added") return redirect('suppliers') @login_required def edit_supplier(request, pk): s = get_object_or_404(Supplier, pk=pk) if request.method == 'POST': s.name = request.POST.get('name') s.contact_person = request.POST.get('contact_person') s.phone = request.POST.get('phone') s.save() messages.success(request, "Supplier updated") return redirect('suppliers') @login_required def delete_supplier(request, pk): get_object_or_404(Supplier, pk=pk).delete() messages.success(request, "Supplier deleted") return redirect('suppliers') # --- Purchases --- @login_required def purchases(request): purchases = Purchase.objects.all().order_by('-created_at') paginator = Paginator(purchases, 25) return render(request, 'core/purchases.html', { 'purchases': paginator.get_page(request.GET.get('page')), 'payment_methods': PaymentMethod.objects.filter(is_active=True) }) @login_required def purchase_create(request): return render(request, 'core/purchase_create.html', { 'suppliers': Supplier.objects.all(), 'products': Product.objects.all(), 'payment_methods': PaymentMethod.objects.filter(is_active=True), 'site_settings': SystemSetting.objects.first() }) @csrf_exempt @login_required def create_purchase_api(request): if request.method != 'POST': return JsonResponse({'success': False, 'error': 'Invalid method'}) try: data = json.loads(request.body) items = data.get('items', []) if not items: return JsonResponse({'success': False, 'error': 'No items'}) with transaction.atomic(): purchase = Purchase.objects.create( supplier_id=data.get('supplier_id'), invoice_number=data.get('invoice_number', ''), total_amount=0, created_by=request.user, notes=data.get('notes', ''), status='paid' if data.get('payment_amount') else 'unpaid' # simplified ) total = 0 for item in items: qty = decimal.Decimal(str(item['quantity'])) cost = decimal.Decimal(str(item.get('price', 0))) line = qty * cost total += line # Update product cost and stock prod = Product.objects.get(pk=item['id']) prod.cost_price = cost # Last purchase price logic prod.stock_quantity += qty prod.save() PurchaseItem.objects.create( purchase=purchase, product=prod, quantity=qty, cost_price=cost, line_total=line ) purchase.total_amount = total # Handle payment pay_amount = decimal.Decimal(str(data.get('payment_amount', 0))) purchase.paid_amount = pay_amount purchase.balance_due = total - pay_amount purchase.status = 'paid' if purchase.balance_due <= 0 else ('partial' if pay_amount > 0 else 'unpaid') purchase.save() if pay_amount > 0: PurchasePayment.objects.create( purchase=purchase, amount=pay_amount, payment_method_id=data.get('payment_method_id'), created_by=request.user ) return JsonResponse({'success': True, 'purchase_id': purchase.id}) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}) @login_required def purchase_detail(request, pk): purchase = get_object_or_404(Purchase, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/purchase_detail.html', { 'purchase': purchase, 'settings': settings, 'payment_methods': PaymentMethod.objects.filter(is_active=True) }) @login_required def delete_purchase(request, pk): get_object_or_404(Purchase, pk=pk).delete() messages.success(request, "Purchase deleted") return redirect('purchases') @login_required def edit_purchase(request, pk): # Stub for now return redirect('purchases') # --- Quotations --- @login_required def quotations(request): quotations = Quotation.objects.all().order_by('-created_at') return render(request, 'core/quotations.html', {'quotations': quotations}) @login_required def quotation_create(request): return render(request, 'core/quotation_create.html', { 'customers': Customer.objects.all(), 'products': Product.objects.all(), 'site_settings': SystemSetting.objects.first() }) @csrf_exempt @login_required def create_quotation_api(request): if request.method != 'POST': return JsonResponse({'success': False}) try: data = json.loads(request.body) with transaction.atomic(): q = Quotation.objects.create( customer_id=data.get('customer_id'), total_amount=0, created_by=request.user, notes=data.get('notes', ''), quotation_number=f"QT-{timezone.now().strftime('%Y%m%d%H%M%S')}" ) total = 0 for item in data.get('items', []): qty = decimal.Decimal(str(item['quantity'])) price = decimal.Decimal(str(item['price'])) line = qty * price total += line QuotationItem.objects.create(quotation=q, product_id=item['id'], quantity=qty, unit_price=price, line_total=line) q.total_amount = total q.save() return JsonResponse({'success': True, 'quotation_id': q.id}) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}) @login_required def quotation_detail(request, pk): quotation = get_object_or_404(Quotation, pk=pk) return render(request, 'core/quotation_detail.html', { 'quotation': quotation, 'settings': SystemSetting.objects.first(), 'amount_in_words': number_to_words_en(quotation.total_amount) }) @login_required def convert_quotation_to_invoice(request, pk): # Logic to convert quotation to sale would go here # For now, just stub it messages.info(request, "Conversion logic not yet fully implemented") return redirect('quotations') @login_required def delete_quotation(request, pk): get_object_or_404(Quotation, pk=pk).delete() return redirect('quotations') # --- Invoices (Sales) --- @login_required def invoice_create(request): settings = SystemSetting.objects.first() context = { 'products': Product.objects.filter(is_active=True), 'customers': Customer.objects.all(), 'payment_methods': PaymentMethod.objects.filter(is_active=True), 'site_settings': settings, 'decimal_places': settings.decimal_places if settings else 3 } return render(request, 'core/invoice_create.html', context) @login_required def invoice_detail(request, pk): sale = get_object_or_404(Sale, pk=pk) settings = SystemSetting.objects.first() amount_in_words = number_to_words_en(sale.total_amount) return render(request, 'core/invoice_detail.html', { 'sale': sale, 'settings': settings, 'amount_in_words': amount_in_words, 'payment_methods': PaymentMethod.objects.filter(is_active=True) }) @login_required def delete_sale(request, pk): get_object_or_404(Sale, pk=pk).delete() return redirect('invoices') @csrf_exempt def update_sale_api(request, pk): return JsonResponse({'success': True}) # --- Expenses --- @login_required def expenses_view(request): expenses = Expense.objects.all().order_by('-date') return render(request, 'core/expenses.html', { 'expenses': expenses, 'categories': ExpenseCategory.objects.all(), 'payment_methods': PaymentMethod.objects.all() }) @login_required def expense_create_view(request): if request.method == 'POST': form = ExpenseForm(request.POST, request.FILES) if form.is_valid(): form.save() messages.success(request, "Expense added") return redirect('expenses') return redirect('expenses') @login_required def expense_edit_view(request, pk): return redirect('expenses') @login_required def expense_delete_view(request, pk): get_object_or_404(Expense, pk=pk).delete() return redirect('expenses') @login_required def expense_categories_view(request): return render(request, 'core/expense_categories.html') @login_required def expense_category_delete_view(request, pk): return redirect('expenses') # --- Payment Methods --- @login_required def add_payment_method(request): return redirect('settings') @login_required def edit_payment_method(request, pk): return redirect('settings') @login_required def delete_payment_method(request, pk): return redirect('settings') @csrf_exempt def add_payment_method_ajax(request): return JsonResponse({'success': True}) # --- Loyalty --- @login_required def add_loyalty_tier(request): return redirect('settings') @login_required def edit_loyalty_tier(request, pk): return redirect('settings') @login_required def delete_loyalty_tier(request, pk): return redirect('settings') @login_required def get_customer_loyalty_api(request, pk): return JsonResponse({'points': 0}) # --- WhatsApp --- @login_required def send_invoice_whatsapp(request): return JsonResponse({'success': True}) @login_required def test_whatsapp_connection(request): return JsonResponse({'success': True}) # --- LPO --- @login_required def lpo_list(request): return render(request, 'core/lpo_list.html', {'lpos': PurchaseOrder.objects.all()}) @login_required def lpo_create(request): return render(request, 'core/lpo_create.html', { 'suppliers': Supplier.objects.all(), 'products': Product.objects.all(), 'site_settings': SystemSetting.objects.first() }) @csrf_exempt @login_required def create_lpo_api(request): if request.method != 'POST': return JsonResponse({'success': False}) try: data = json.loads(request.body) with transaction.atomic(): lpo = PurchaseOrder.objects.create( supplier_id=data.get('supplier_id'), total_amount=0, created_by=request.user, lpo_number=f"LPO-{timezone.now().strftime('%Y%m%d%H%M%S')}" ) total = 0 for item in data.get('items', []): qty = decimal.Decimal(str(item['quantity'])) cost = decimal.Decimal(str(item.get('price', 0))) line = qty * cost total += line PurchaseOrderItem.objects.create(purchase_order=lpo, product_id=item['id'], quantity=qty, cost_price=cost, line_total=line) lpo.total_amount = total lpo.save() return JsonResponse({'success': True, 'lpo_id': lpo.id}) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}) @login_required def lpo_detail(request, pk): lpo = get_object_or_404(PurchaseOrder, pk=pk) return render(request, 'core/lpo_detail.html', { 'lpo': lpo, 'settings': SystemSetting.objects.first() }) @login_required def convert_lpo_to_purchase(request, pk): return redirect('lpo_list') @login_required def lpo_delete(request, pk): get_object_or_404(PurchaseOrder, pk=pk).delete() return redirect('lpo_list') # --- Sales Returns --- @login_required def sales_returns(request): return render(request, 'core/sales_returns.html', {'returns': SaleReturn.objects.all()}) @login_required def sale_return_create(request): return render(request, 'core/sale_return_create.html') @login_required def sale_return_detail(request, pk): sale_return = get_object_or_404(SaleReturn, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/sale_return_detail.html', { 'sale_return': sale_return, 'settings': settings }) @login_required def delete_sale_return(request, pk): return redirect('sales_returns') @csrf_exempt def create_sale_return_api(request): return JsonResponse({'success': True}) # --- Purchase Returns --- @login_required def purchase_returns(request): return render(request, 'core/purchase_returns.html', {'returns': PurchaseReturn.objects.all()}) @login_required def purchase_return_create(request): return render(request, 'core/purchase_return_create.html') @login_required def purchase_return_detail(request, pk): purchase_return = get_object_or_404(PurchaseReturn, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/purchase_return_detail.html', { 'purchase_return': purchase_return, 'settings': settings }) @login_required def delete_purchase_return(request, pk): return redirect('purchase_returns') @csrf_exempt def create_purchase_return_api(request): return JsonResponse({'success': True}) # --- Other Stubs --- @login_required def customer_statement(request): return render(request, 'core/customer_statement.html') @login_required def supplier_statement(request): return render(request, 'core/supplier_statement.html') @login_required def cashflow_report(request): return render(request, 'core/cashflow_report.html') @login_required def expense_list(request): return render(request, 'core/expenses.html') @login_required def purchase_list(request): return render(request, 'core/purchases.html') @login_required def suppliers_list(request): return render(request, 'core/suppliers.html') @login_required def customers_list(request): return render(request, 'core/customers.html') @login_required def add_device(request): return redirect('settings') @login_required def edit_device(request, pk): return redirect('settings') @login_required def delete_device(request, pk): return redirect('settings') @csrf_exempt def pos_sync_update(request): return JsonResponse({'status': 'ok'}) @csrf_exempt def pos_sync_state(request): return JsonResponse({'state': {}}) @login_required def customer_display(request): return render(request, 'core/customer_display.html') @login_required def supplier_payments(request): return render(request, 'core/supplier_payments.html') @csrf_exempt def update_purchase_api(request, pk): return JsonResponse({'success': True}) @login_required def add_sale_payment(request, pk): sale = get_object_or_404(Sale, pk=pk) if request.method == 'POST': try: amount = decimal.Decimal(request.POST.get('amount', 0)) payment_method_id = request.POST.get('payment_method_id') notes = request.POST.get('notes', '') if amount > 0: with transaction.atomic(): SalePayment.objects.create( sale=sale, amount=amount, payment_method_id=payment_method_id, created_by=request.user, notes=notes ) # Recalculate totals total_paid = SalePayment.objects.filter(sale=sale).aggregate(Sum('amount'))['amount__sum'] or 0 sale.paid_amount = total_paid sale.balance_due = sale.total_amount - total_paid if sale.balance_due <= 0: sale.status = 'paid' elif sale.paid_amount > 0: sale.status = 'partial' else: sale.status = 'unpaid' sale.save() messages.success(request, f"Payment of {amount} recorded successfully.") else: messages.error(request, "Amount must be greater than 0.") except Exception as e: messages.error(request, f"Error recording payment: {e}") return redirect('invoices') @login_required def sale_receipt(request, pk): return render(request, 'core/sale_receipt.html') @login_required def edit_invoice(request, pk): return redirect('invoices') @login_required def customer_payments(request): return render(request, 'core/customer_payments.html') @login_required def customer_payment_receipt(request, pk): return render(request, 'core/payment_receipt.html') @csrf_exempt def hold_sale_api(request): return JsonResponse({'success': True}) @csrf_exempt def get_held_sales_api(request): return JsonResponse({'sales': []}) @csrf_exempt def recall_held_sale_api(request, pk): return JsonResponse({'success': True}) @csrf_exempt def delete_held_sale_api(request, pk): return JsonResponse({'success': True}) @login_required def add_purchase_payment(request, pk): purchase = get_object_or_404(Purchase, pk=pk) if request.method == 'POST': try: amount = decimal.Decimal(request.POST.get('amount', 0)) payment_method_id = request.POST.get('payment_method_id') notes = request.POST.get('notes', '') if amount > 0: with transaction.atomic(): PurchasePayment.objects.create( purchase=purchase, amount=amount, payment_method_id=payment_method_id, created_by=request.user, notes=notes ) # Recalculate totals total_paid = PurchasePayment.objects.filter(purchase=purchase).aggregate(Sum('amount'))['amount__sum'] or 0 purchase.paid_amount = total_paid purchase.balance_due = purchase.total_amount - total_paid if purchase.balance_due <= 0: purchase.status = 'paid' elif purchase.paid_amount > 0: purchase.status = 'partial' else: purchase.status = 'unpaid' purchase.save() messages.success(request, f"Payment of {amount} recorded successfully.") else: messages.error(request, "Amount must be greater than 0.") except Exception as e: messages.error(request, f"Error recording payment: {e}") return redirect('purchases') @login_required def cashier_registry(request): return render(request, 'core/cashier_registry.html') @login_required def cashier_session_list(request): return render(request, 'core/session_list.html') @login_required def start_session(request): return redirect('pos') @login_required def close_session(request): return redirect('index') @login_required def session_detail(request, pk): return render(request, 'core/session_detail.html') @login_required def reports(request): return render(request, 'core/reports.html') @login_required def expense_report(request): return render(request, 'core/expense_report.html') @login_required def export_expenses_excel(request): return redirect('expenses') @login_required def profile_view(request): return render(request, 'core/profile.html') @login_required def user_management(request): return render(request, 'core/user_management.html') @csrf_exempt @login_required def group_details_api(request, pk): return JsonResponse({'success': True, 'group': {}}) @csrf_exempt @login_required def search_customers_api(request): query = request.GET.get('q', '') customers = Customer.objects.filter(name__icontains=query)[:10] data = [{'id': c.id, 'name': c.name, 'phone': c.phone} for c in customers] return JsonResponse({'customers': data})