from django.db import models, transaction from django.utils.translation import gettext_lazy as _ from django.utils import timezone from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver import base64 import os from django.conf import settings as django_settings from django.utils.translation import gettext as _, get_language from django.utils.formats import date_format # Changed to use helpers to avoid circular imports and requests issue from .helpers import number_to_words_en, send_whatsapp_document from django.core.paginator import Paginator import decimal from django.contrib.auth.models import User, Group, Permission 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, Q from django.db.models.functions import TruncDate, TruncMonth from django.http import JsonResponse, HttpResponse from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.decorators import login_required from .models import ( Expense, ExpenseCategory, Product, Sale, Category, Unit, Customer, Supplier, Purchase, PurchaseItem, PurchasePayment, SaleItem, SalePayment, SystemSetting, Quotation, QuotationItem, SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem, PurchaseOrder, PurchaseOrderItem, PaymentMethod, HeldSale, LoyaltyTier, LoyaltyTransaction, Device, CashierCounterRegistry, CashierSession ) import json from datetime import timedelta from django.utils import timezone from django.contrib import messages from django.utils.text import slugify import openpyxl import csv # Forced update to trigger reload # Fixed imports to use helpers @login_required def index(request): """ Enhanced Meezan Dashboard View """ total_products = Product.objects.count() total_sales_count = Sale.objects.count() total_sales_amount = Sale.objects.aggregate(total=Sum('total_amount'))['total'] or 0 total_customers = Customer.objects.count() site_settings = SystemSetting.objects.first() # --- Charts & Analytics Data --- # 1. Monthly Sales (Last 6 months) today = timezone.now().date() six_months_ago = today - timedelta(days=180) monthly_sales = Sale.objects.filter(created_at__date__gte=six_months_ago) \ .annotate(month=TruncMonth('created_at')) \ .values('month') \ .annotate(total=Sum('total_amount')) \ .order_by('month') monthly_labels = [] monthly_data = [] current_lang = get_language() for entry in monthly_sales: dt = entry['month'] # Format: "Jan 2026" or localized equivalent monthly_labels.append(date_format(dt, "M Y")) monthly_data.append(float(entry['total'])) # 2. Daily Sales (Last 7 days) last_week = today - timedelta(days=7) daily_sales = Sale.objects.filter(created_at__date__gte=last_week) \ .annotate(day=TruncDate('created_at')) \ .values('day') \ .annotate(total=Sum('total_amount')) \ .order_by('day') chart_labels = [] chart_data = [] # Fill in missing days for a smooth line chart days_map = {entry['day']: entry['total'] for entry in daily_sales} for i in range(7): d = last_week + timedelta(days=i) chart_labels.append(date_format(d, "M d")) # "Feb 07" chart_data.append(float(days_map.get(d, 0))) # 3. Sales by Category category_sales = SaleItem.objects.values( 'product__category__name_en', 'product__category__name_ar' ).annotate(total=Sum('line_total')).order_by('-total')[:5] category_labels = [] category_data = [] for item in category_sales: name_en = item['product__category__name_en'] or "Uncategorized" name_ar = item['product__category__name_ar'] or name_en label = name_ar if current_lang == 'ar' else name_en category_labels.append(label) category_data.append(float(item['total'])) # 4. 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] # 5. Payment Methods payment_stats = SalePayment.objects.values('payment_method_name').annotate(count=Count('id')) payment_labels = [] payment_data = [] for stat in payment_stats: method_name = stat['payment_method_name'] # Simple translation/mapping for known methods if method_name: if method_name.lower() == 'cash': label = _('Cash') elif method_name.lower() == 'card': label = _('Card') else: label = method_name else: label = _('Unknown') payment_labels.append(str(label)) payment_data.append(stat['count']) # --- Inventory Alerts --- low_stock_threshold = 10 low_stock_products = Product.objects.filter(stock_quantity__lte=F('min_stock_level')).select_related('category')[:5] low_stock_count = Product.objects.filter(stock_quantity__lte=F('min_stock_level')).count() expired_count = Product.objects.filter(expiry_date__lt=today).count() # --- Recent Transactions --- recent_sales = Sale.objects.select_related('customer', 'created_by').order_by('-created_at')[:5] context = { 'total_products': total_products, 'total_sales_count': total_sales_count, 'total_sales_amount': total_sales_amount, 'total_customers': total_customers, 'site_settings': site_settings, 'monthly_labels': json.dumps(monthly_labels), 'monthly_data': json.dumps(monthly_data), 'chart_labels': json.dumps(chart_labels), 'chart_data': json.dumps(chart_data), 'category_labels': json.dumps(category_labels), 'category_data': json.dumps(category_data), 'top_products': top_products, 'payment_labels': json.dumps(payment_labels), 'payment_data': json.dumps(payment_data), '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) @login_required def inventory(request): products = Product.objects.all().order_by('-id') categories = Category.objects.all() units = Unit.objects.all() # Filter by Category category_id = request.GET.get('category') if category_id: products = products.filter(category_id=category_id) # Filter by Search search_query = request.GET.get('search') if search_query: products = products.filter( Q(name_en__icontains=search_query) | Q(name_ar__icontains=search_query) | Q(sku__icontains=search_query) ) paginator = Paginator(products, 25) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) context = { 'products': page_obj, 'categories': categories, 'units': units, } return render(request, 'core/inventory.html', context) @login_required def customers(request): customers = Customer.objects.all().order_by('-id') paginator = Paginator(customers, 25) return render(request, 'core/customers.html', {'customers': paginator.get_page(request.GET.get('page'))}) @login_required def suppliers(request): suppliers = Supplier.objects.all().order_by('-id') paginator = Paginator(suppliers, 25) return render(request, 'core/suppliers.html', {'suppliers': paginator.get_page(request.GET.get('page'))}) @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'))}) @login_required def purchase_detail(request, pk): purchase = get_object_or_404(Purchase, pk=pk) return render(request, 'core/purchase_detail.html', {'purchase': purchase, 'settings': SystemSetting.objects.first()}) @login_required def purchase_create(request): # Stub for purchase creation - redirects to list for now return redirect('purchases') @login_required def add_product(request): if request.method == 'POST': # Quick add logic for simplified form name_en = request.POST.get('name_en') sku = request.POST.get('sku') price = request.POST.get('price') cost_price = request.POST.get('cost_price') stock = request.POST.get('stock_quantity') category_id = request.POST.get('category') try: Product.objects.create( name_en=name_en, sku=sku, price=price, cost_price=cost_price, stock_quantity=stock, category_id=category_id, created_by=request.user ) messages.success(request, 'Product added successfully.') except Exception as e: messages.error(request, str(e)) return redirect('inventory') @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.price = request.POST.get('price') product.cost_price = request.POST.get('cost_price') product.stock_quantity = request.POST.get('stock_quantity') product.min_stock_level = request.POST.get('min_stock_level') or 0 product.category_id = request.POST.get('category') product.save() messages.success(request, 'Product updated.') return redirect('inventory') # Render edit form if needed, but for now redirecting return redirect('inventory') @login_required def delete_product(request, pk): product = get_object_or_404(Product, pk=pk) product.delete() messages.success(request, 'Product deleted.') return redirect('inventory') @login_required def barcode_labels(request): # Logic to print barcodes return render(request, 'core/barcode_labels.html') @login_required def import_products(request): # Logic to import from Excel return redirect('inventory') @login_required def pos(request): # Ensure a session is active for this user/device # Check if this user has an open session # For now, we'll just show the POS products = Product.objects.filter(is_active=True).select_related('category') categories = Category.objects.all() customers = Customer.objects.all() payment_methods = PaymentMethod.objects.filter(is_active=True) settings = SystemSetting.objects.first() context = { 'products': products, 'categories': categories, 'customers': customers, 'payment_methods': payment_methods, 'settings': settings, } return render(request, 'core/pos.html', context) @login_required def customer_display(request): return render(request, 'core/customer_display.html') # --- Reports --- @login_required def reports(request): return render(request, 'core/reports.html') @login_required def customer_statement(request): customers = Customer.objects.all() selected_customer = None transactions = [] if request.GET.get('customer'): selected_customer = get_object_or_404(Customer, pk=request.GET.get('customer')) # Gather sales, payments, etc. sales = Sale.objects.filter(customer=selected_customer).annotate(type=models.Value('Sale', output_field=models.CharField())) payments = SalePayment.objects.filter(sale__customer=selected_customer).annotate(type=models.Value('Payment', output_field=models.CharField())) # Merge and sort by date... (Simplified) transactions = list(sales) + list(payments) transactions.sort(key=lambda x: x.created_at, reverse=True) return render(request, 'core/customer_statement.html', {'customers': customers, 'selected_customer': selected_customer, 'transactions': transactions}) @login_required def supplier_statement(request): suppliers = Supplier.objects.all() return render(request, 'core/supplier_statement.html', {'suppliers': suppliers}) @login_required def cashflow_report(request): sales = Sale.objects.all() expenses = Expense.objects.all() purchases = Purchase.objects.all() if request.GET.get('start_date'): sales = sales.filter(created_at__date__gte=request.GET.get('start_date')) expenses = expenses.filter(date__gte=request.GET.get('start_date')) purchases = purchases.filter(created_at__date__gte=request.GET.get('start_date')) if request.GET.get('end_date'): sales = sales.filter(created_at__date__lte=request.GET.get('end_date')) expenses = expenses.filter(date__lte=request.GET.get('end_date')) purchases = purchases.filter(created_at__date__lte=request.GET.get('end_date')) total_sales = sales.aggregate(total=Sum('total_amount'))['total'] or 0 total_expenses = expenses.aggregate(total=Sum('amount'))['total'] or 0 total_purchases = purchases.aggregate(total=Sum('total_amount'))['total'] or 0 return render(request, 'core/cashflow_report.html', {'total_sales': total_sales, 'total_expenses': total_expenses, 'total_purchases': total_purchases, 'net_profit': total_sales - total_expenses - total_purchases}) @login_required def invoice_list(request): sales = Sale.objects.all().order_by('-created_at') # Filter by date range start_date = request.GET.get('start_date') end_date = request.GET.get('end_date') if start_date: sales = sales.filter(created_at__date__gte=start_date) if end_date: sales = sales.filter(created_at__date__lte=end_date) # Filter by customer customer_id = request.GET.get('customer') if customer_id: sales = sales.filter(customer_id=customer_id) # Filter by status status = request.GET.get('status') if status: sales = sales.filter(status=status) paginator = Paginator(sales, 25) context = { 'sales': paginator.get_page(request.GET.get('page')), 'customers': Customer.objects.all(), 'payment_methods': PaymentMethod.objects.filter(is_active=True), 'site_settings': SystemSetting.objects.first(), } return render(request, 'core/invoices.html', context) @login_required def invoice_detail(request, pk): return render(request, 'core/invoice_detail.html', {'sale': get_object_or_404(Sale, pk=pk), 'settings': SystemSetting.objects.first()}) @login_required def invoice_create(request): customers = Customer.objects.all() products = Product.objects.filter(is_active=True).select_related('category') payment_methods = PaymentMethod.objects.filter(is_active=True) site_settings = SystemSetting.objects.first() decimal_places = 2 if site_settings: decimal_places = site_settings.decimal_places context = { 'customers': customers, 'products': products, 'payment_methods': payment_methods, 'site_settings': site_settings, 'decimal_places': decimal_places, } return render(request, 'core/invoice_create.html', context) # --- STUBS & MISSING VIEWS --- @login_required def quotations(request): return render(request, 'core/quotations.html') @login_required def quotation_create(request): return redirect('quotations') @login_required def quotation_detail(request, pk): return redirect('quotations') @login_required def convert_quotation_to_invoice(request, pk): return redirect('quotations') @login_required def delete_quotation(request, pk): return redirect('quotations') @csrf_exempt def create_quotation_api(request): return JsonResponse({'success': False}) @login_required def sales_returns(request): return render(request, 'core/sales_returns.html') @login_required def sale_return_create(request): return redirect('sales_returns') @login_required def sale_return_detail(request, pk): return redirect('sales_returns') @login_required def delete_sale_return(request, pk): return redirect('sales_returns') @csrf_exempt def create_sale_return_api(request): return JsonResponse({'success': False}) @login_required def add_purchase_payment(request, pk): return redirect('purchases') @login_required def delete_purchase(request, pk): return redirect('purchases') @login_required def purchase_returns(request): return render(request, 'core/purchase_returns.html') @login_required def purchase_return_create(request): return redirect('purchase_returns') @login_required def purchase_return_detail(request, pk): return redirect('purchase_returns') @login_required def delete_purchase_return(request, pk): return redirect('purchase_returns') @csrf_exempt def create_purchase_return_api(request): return JsonResponse({'success': False}) @login_required def export_expenses_excel(request): return redirect('expenses') @csrf_exempt def update_sale_api(request, pk): if request.method != 'POST': return JsonResponse({'success': False, 'error': 'Invalid request method'}) try: sale = Sale.objects.get(pk=pk) data = json.loads(request.body) customer_id = data.get('customer_id') items = data.get('items', []) discount = decimal.Decimal(str(data.get('discount', 0))) paid_amount = decimal.Decimal(str(data.get('paid_amount', 0))) payment_type = data.get('payment_type', 'cash') payment_method_id = data.get('payment_method_id') due_date = data.get('due_date') notes = data.get('notes', '') invoice_number = data.get('invoice_number') if not items: return JsonResponse({'success': False, 'error': 'No items in sale'}) with transaction.atomic(): # 1. Revert Stock for item in sale.items.all(): product = item.product product.stock_quantity += item.quantity product.save() # 2. Delete existing items sale.items.all().delete() # 3. Update Sale Details if customer_id: sale.customer_id = customer_id else: sale.customer = None sale.discount = discount sale.notes = notes if invoice_number: sale.invoice_number = invoice_number if due_date: sale.due_date = due_date else: sale.due_date = None # 4. Create New Items and Deduct Stock subtotal = decimal.Decimal(0) for item_data in items: product = Product.objects.get(pk=item_data['id']) quantity = decimal.Decimal(str(item_data['quantity'])) price = decimal.Decimal(str(item_data['price'])) # Deduct stock product.stock_quantity -= quantity product.save() line_total = price * quantity subtotal += line_total SaleItem.objects.create( sale=sale, product=product, quantity=quantity, unit_price=price, line_total=line_total ) sale.subtotal = subtotal sale.total_amount = subtotal - discount # 5. Handle Payments if payment_type == 'credit': sale.status = 'unpaid' sale.paid_amount = 0 sale.balance_due = sale.total_amount sale.payments.all().delete() elif payment_type == 'cash': sale.status = 'paid' sale.paid_amount = sale.total_amount sale.balance_due = 0 sale.payments.all().delete() SalePayment.objects.create( sale=sale, amount=sale.total_amount, payment_method_id=payment_method_id if payment_method_id else None, payment_date=timezone.now().date(), notes='Full Payment (Edit)' ) elif payment_type == 'partial': sale.paid_amount = paid_amount sale.balance_due = sale.total_amount - paid_amount if sale.balance_due <= 0: sale.status = 'paid' sale.balance_due = 0 else: sale.status = 'partial' sale.payments.all().delete() SalePayment.objects.create( sale=sale, amount=paid_amount, payment_method_id=payment_method_id if payment_method_id else None, payment_date=timezone.now().date(), notes='Partial Payment (Edit)' ) sale.save() return JsonResponse({'success': True, 'sale_id': sale.id}) except Sale.DoesNotExist: return JsonResponse({'success': False, 'error': 'Sale not found'}) except Product.DoesNotExist: return JsonResponse({'success': False, 'error': 'Product not found'}) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}) @csrf_exempt def hold_sale_api(request): return JsonResponse({'success': False}) @csrf_exempt def get_held_sales_api(request): return JsonResponse({'sales': []}) @csrf_exempt def recall_held_sale_api(request, pk): return JsonResponse({'success': False}) @csrf_exempt def delete_held_sale_api(request, pk): return JsonResponse({'success': False}) @login_required def add_customer(request): return redirect('customers') @login_required def edit_customer(request, pk): return redirect('customers') @login_required def delete_customer(request, pk): return redirect('customers') @csrf_exempt def add_customer_ajax(request): return JsonResponse({'success': False}) @login_required def add_supplier(request): return redirect('suppliers') @login_required def edit_supplier(request, pk): return redirect('suppliers') @login_required def delete_supplier(request, pk): return redirect('suppliers') @csrf_exempt def add_supplier_ajax(request): return JsonResponse({'success': False}) @login_required def suggest_sku(request): return JsonResponse({'sku': '12345'}) @login_required def add_category(request): return redirect('inventory') @login_required def edit_category(request, pk): return redirect('inventory') @login_required def delete_category(request, pk): return redirect('inventory') @csrf_exempt def add_category_ajax(request): return JsonResponse({'success': False}) @login_required def import_categories(request): return redirect('inventory') @login_required def add_unit(request): return redirect('inventory') @login_required def edit_unit(request, pk): return redirect('inventory') @login_required def delete_unit(request, pk): return redirect('inventory') @csrf_exempt def add_unit_ajax(request): return JsonResponse({'success': False}) @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': False}) @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') @csrf_exempt def get_customer_loyalty_api(request, pk): return JsonResponse({'success': False}) @csrf_exempt def send_invoice_whatsapp(request): return JsonResponse({'success': False}) @csrf_exempt def test_whatsapp_connection(request): return JsonResponse({'success': False}) @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') @login_required def lpo_list(request): return render(request, 'core/lpo_list.html') @login_required def lpo_create(request): return redirect('lpo_list') @login_required def lpo_detail(request, pk): return redirect('lpo_list') @login_required def convert_lpo_to_purchase(request, pk): return redirect('lpo_list') @login_required def lpo_delete(request, pk): return redirect('lpo_list') @csrf_exempt def create_lpo_api(request): return JsonResponse({'success': False}) @login_required def cashier_registry(request): return redirect('settings') @login_required def cashier_session_list(request): return render(request, 'core/cashier_sessions.html') @login_required def start_session(request): return redirect('cashier_session_list') @login_required def close_session(request): return redirect('cashier_session_list') @login_required def session_detail(request, pk): return redirect('cashier_session_list') @login_required def expenses_view(request): expenses = Expense.objects.all().select_related('category', 'payment_method', 'created_by').order_by('-date') categories = ExpenseCategory.objects.all() payment_methods = PaymentMethod.objects.filter(is_active=True) paginator = Paginator(expenses, 25) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) context = { 'expenses': page_obj, 'categories': categories, 'payment_methods': payment_methods, } return render(request, 'core/expenses.html', context) @login_required def expense_create_view(request): if request.method == 'POST': try: category_id = request.POST.get('category') amount = request.POST.get('amount') date = request.POST.get('date') description = request.POST.get('description') payment_method_id = request.POST.get('payment_method') category = get_object_or_404(ExpenseCategory, pk=category_id) payment_method = get_object_or_404(PaymentMethod, pk=payment_method_id) if payment_method_id else None expense = Expense.objects.create( category=category, amount=amount, date=date or timezone.now().date(), description=description, payment_method=payment_method, created_by=request.user ) if 'attachment' in request.FILES: expense.attachment = request.FILES['attachment'] expense.save() messages.success(request, _('Expense added successfully.')) except Exception as e: messages.error(request, _('Error adding expense: ') + str(e)) return redirect('expenses') @login_required def expense_edit_view(request, pk): expense = get_object_or_404(Expense, pk=pk) if request.method == 'POST': try: category_id = request.POST.get('category') amount = request.POST.get('amount') date = request.POST.get('date') description = request.POST.get('description') payment_method_id = request.POST.get('payment_method') category = get_object_or_404(ExpenseCategory, pk=category_id) payment_method = get_object_or_404(PaymentMethod, pk=payment_method_id) if payment_method_id else None expense.category = category expense.amount = amount expense.date = date or expense.date expense.description = description expense.payment_method = payment_method if 'attachment' in request.FILES: expense.attachment = request.FILES['attachment'] expense.save() messages.success(request, _('Expense updated successfully.')) except Exception as e: messages.error(request, _('Error updating expense: ') + str(e)) return redirect('expenses') @login_required def expense_delete_view(request, pk): expense = get_object_or_404(Expense, pk=pk) expense.delete() messages.success(request, _('Expense deleted successfully.')) return redirect('expenses') @login_required def expense_categories_view(request): if request.method == 'POST': category_id = request.POST.get('category_id') name_en = request.POST.get('name_en') name_ar = request.POST.get('name_ar') description = request.POST.get('description') if category_id: # Update existing category category = get_object_or_404(ExpenseCategory, pk=category_id) category.name_en = name_en category.name_ar = name_ar category.description = description category.save() messages.success(request, _('Expense category updated successfully.')) else: # Create new category ExpenseCategory.objects.create( name_en=name_en, name_ar=name_ar, description=description ) messages.success(request, _('Expense category added successfully.')) return redirect('expense_categories') categories = ExpenseCategory.objects.all().order_by('-id') return render(request, 'core/expense_categories.html', {'categories': categories}) @login_required def expense_category_delete_view(request, pk): category = get_object_or_404(ExpenseCategory, pk=pk) if category.expenses.exists(): messages.error(request, _('Cannot delete category because it has related expenses.')) else: category.delete() messages.success(request, _('Expense category deleted successfully.')) return redirect('expense_categories') @login_required def expense_report(request): return render(request, 'core/expense_report.html') @login_required def customer_payments(request): return redirect('invoices') @login_required def customer_payment_receipt(request, pk): return redirect('invoices') @login_required def sale_receipt(request, pk): sale = get_object_or_404(Sale, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/sale_receipt.html', { 'sale': sale, 'settings': settings }) @login_required def edit_invoice(request, pk): sale = get_object_or_404(Sale, pk=pk) customers = Customer.objects.all() products = Product.objects.filter(is_active=True).select_related('category') payment_methods = PaymentMethod.objects.filter(is_active=True) site_settings = SystemSetting.objects.first() decimal_places = 2 if site_settings: decimal_places = site_settings.decimal_places # Serialize items for Vue cart_items = [] for item in sale.items.all().select_related('product'): cart_items.append({ 'id': item.product.id, 'name_en': item.product.name_en, 'name_ar': item.product.name_ar, 'sku': item.product.sku, 'price': float(item.unit_price), 'quantity': float(item.quantity), 'stock': float(item.product.stock_quantity) }) cart_json = json.dumps(cart_items) # Get first payment method if exists payment_method_id = "" first_payment = sale.payments.first() if first_payment and first_payment.payment_method: payment_method_id = first_payment.payment_method.id context = { 'sale': sale, 'customers': customers, 'products': products, 'payment_methods': payment_methods, 'site_settings': site_settings, 'decimal_places': decimal_places, 'cart_json': cart_json, 'payment_method_id': payment_method_id } return render(request, 'core/invoice_edit.html', context) @login_required def add_sale_payment(request, pk): return redirect('invoices') @login_required def delete_sale(request, pk): return redirect('invoices') @login_required def supplier_payments(request): return redirect('purchases') @login_required def settings_view(request): settings, created = SystemSetting.objects.get_or_create(id=1) if request.method == 'POST': # Business Profile settings.business_name = request.POST.get('business_name', '') settings.email = request.POST.get('email', '') settings.phone = request.POST.get('phone', '') settings.vat_number = request.POST.get('vat_number', '') settings.registration_number = request.POST.get('registration_number', '') settings.address = request.POST.get('address', '') # Financial settings.currency_symbol = request.POST.get('currency_symbol', 'OMR') settings.tax_rate = request.POST.get('tax_rate', 0) settings.decimal_places = request.POST.get('decimal_places', 3) settings.allow_zero_stock_sales = request.POST.get('allow_zero_stock_sales') == 'on' # Loyalty settings.loyalty_enabled = request.POST.get('loyalty_enabled') == 'on' settings.min_points_to_redeem = request.POST.get('min_points_to_redeem', 100) settings.points_per_currency = request.POST.get('points_per_currency', 1.0) settings.currency_per_point = request.POST.get('currency_per_point', 0.010) # WhatsApp (Wablas) settings.wablas_enabled = request.POST.get('wablas_enabled') == 'on' settings.wablas_server_url = request.POST.get('wablas_server_url', '') settings.wablas_token = request.POST.get('wablas_token', '') settings.wablas_secret_key = request.POST.get('wablas_secret_key', '') # Logo Upload if 'logo' in request.FILES: settings.logo = request.FILES['logo'] settings.save() messages.success(request, _('System settings updated successfully.')) return redirect('settings') payment_methods = PaymentMethod.objects.all() devices = Device.objects.all() loyalty_tiers = LoyaltyTier.objects.all() return render(request, 'core/settings.html', { 'settings': settings, 'payment_methods': payment_methods, 'devices': devices, 'loyalty_tiers': loyalty_tiers, }) @login_required def profile_view(request): return render(request, 'core/profile.html') @login_required def user_management(request): return render(request, 'core/users.html') @csrf_exempt def group_details_api(request, pk): return JsonResponse({'success': False}) @csrf_exempt def create_sale_api(request): if request.method != 'POST': return JsonResponse({'success': False, 'error': 'Invalid request method'}) try: data = json.loads(request.body) customer_id = data.get('customer_id') items = data.get('items', []) discount = decimal.Decimal(str(data.get('discount', 0))) paid_amount = decimal.Decimal(str(data.get('paid_amount', 0))) payment_type = data.get('payment_type', 'cash') payment_method_id = data.get('payment_method_id') due_date = data.get('due_date') notes = data.get('notes', '') invoice_number = data.get('invoice_number') if not items: return JsonResponse({'success': False, 'error': 'No items in sale'}) with transaction.atomic(): customer = None if customer_id: customer = Customer.objects.get(pk=customer_id) # Calculate totals server-side for security subtotal = decimal.Decimal(0) vat_amount = decimal.Decimal(0) sale = Sale( customer=customer, created_by=request.user, status='unpaid', discount=discount, notes=notes, invoice_number=invoice_number ) if due_date: sale.due_date = due_date sale.save() for item in items: product = Product.objects.select_for_update().get(pk=item['id']) qty = decimal.Decimal(str(item['quantity'])) unit_price = decimal.Decimal(str(item['price'])) # Check stock if product.stock_quantity < qty: settings = SystemSetting.objects.first() if not settings or not settings.allow_zero_stock_sales: raise Exception(f"Insufficient stock for {product.name_en}") line_total = unit_price * qty line_vat = line_total * (product.vat / 100) if product.vat else 0 SaleItem.objects.create( sale=sale, product=product, quantity=qty, unit_price=unit_price, line_total=line_total ) product.stock_quantity -= qty product.save() subtotal += line_total vat_amount += decimal.Decimal(line_vat) sale.subtotal = subtotal sale.vat_amount = vat_amount sale.total_amount = subtotal + vat_amount - discount if payment_type == 'credit': sale.status = 'unpaid' elif paid_amount >= sale.total_amount: sale.status = 'paid' else: sale.status = 'partial' sale.save() if paid_amount > 0 and payment_type != 'credit': payment_method = None if payment_method_id: payment_method = PaymentMethod.objects.get(pk=payment_method_id) SalePayment.objects.create( sale=sale, amount=paid_amount, payment_method=payment_method, payment_date=timezone.now(), created_by=request.user, notes=f"Initial payment ({payment_type})" ) return JsonResponse({'success': True, 'sale_id': sale.id}) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}) @csrf_exempt def create_purchase_api(request): return JsonResponse({'success': False}) @csrf_exempt def search_customers_api(request): return JsonResponse({'customers': []}) @login_required def pos_sync_update(request): return JsonResponse({'success': False}) @login_required def pos_sync_state(request): return JsonResponse({'success': False}) @login_required def edit_purchase(request, pk): purchase = get_object_or_404(Purchase, pk=pk) suppliers = Supplier.objects.all() products = Product.objects.filter(is_active=True).select_related('category') payment_methods = PaymentMethod.objects.filter(is_active=True) site_settings = SystemSetting.objects.first() decimal_places = 2 if site_settings: decimal_places = site_settings.decimal_places # Serialize items for Vue cart_items = [] for item in purchase.items.all().select_related('product'): cart_items.append({ 'id': item.product.id, 'name_en': item.product.name_en, 'name_ar': item.product.name_ar, 'sku': item.product.sku, 'cost_price': float(item.cost_price), 'quantity': float(item.quantity), 'stock': float(item.product.stock_quantity) }) cart_json = json.dumps(cart_items) # Get first payment method if exists payment_method_id = "" first_payment = purchase.payments.first() if first_payment and first_payment.payment_method: payment_method_id = first_payment.payment_method.id context = { 'purchase': purchase, 'suppliers': suppliers, 'products': products, 'payment_methods': payment_methods, 'site_settings': site_settings, 'decimal_places': decimal_places, 'cart_json': cart_json, 'payment_method_id': payment_method_id } return render(request, 'core/purchase_edit.html', context) @csrf_exempt def update_purchase_api(request, pk): if request.method != 'POST': return JsonResponse({'success': False, 'error': 'Invalid request method'}) try: purchase = Purchase.objects.get(pk=pk) data = json.loads(request.body) supplier_id = data.get('supplier_id') items = data.get('items', []) paid_amount = decimal.Decimal(str(data.get('paid_amount', 0))) payment_type = data.get('payment_type', 'cash') payment_method_id = data.get('payment_method_id') due_date = data.get('due_date') notes = data.get('notes', '') invoice_number = data.get('invoice_number') if not items: return JsonResponse({'success': False, 'error': 'No items in purchase'}) with transaction.atomic(): # 1. Revert Stock (Subtract what was added) for item in purchase.items.all(): product = item.product product.stock_quantity -= item.quantity product.save() # 2. Delete existing items purchase.items.all().delete() # 3. Update Purchase Details if supplier_id: purchase.supplier_id = supplier_id else: purchase.supplier = None purchase.notes = notes if invoice_number: purchase.invoice_number = invoice_number if due_date: purchase.due_date = due_date else: purchase.due_date = None # 4. Create New Items and Add Stock total_amount = decimal.Decimal(0) for item_data in items: product = Product.objects.get(pk=item_data['id']) quantity = decimal.Decimal(str(item_data['quantity'])) cost_price = decimal.Decimal(str(item_data['cost_price'])) # Add stock product.stock_quantity += quantity # Update product cost price (optional, but good practice to update to latest) product.cost_price = cost_price product.save() line_total = cost_price * quantity total_amount += line_total PurchaseItem.objects.create( purchase=purchase, product=product, quantity=quantity, cost_price=cost_price, line_total=line_total ) purchase.total_amount = total_amount # 5. Handle Payments if payment_type == 'credit': purchase.status = 'unpaid' purchase.paid_amount = 0 purchase.balance_due = purchase.total_amount purchase.payments.all().delete() elif payment_type == 'cash': purchase.status = 'paid' purchase.paid_amount = purchase.total_amount purchase.balance_due = 0 purchase.payments.all().delete() PurchasePayment.objects.create( purchase=purchase, amount=purchase.total_amount, payment_method_id=payment_method_id if payment_method_id else None, payment_date=timezone.now().date(), notes='Full Payment (Edit)' ) elif payment_type == 'partial': purchase.paid_amount = paid_amount purchase.balance_due = purchase.total_amount - paid_amount if purchase.balance_due <= 0: purchase.status = 'paid' purchase.balance_due = 0 else: purchase.status = 'partial' purchase.payments.all().delete() PurchasePayment.objects.create( purchase=purchase, amount=paid_amount, payment_method_id=payment_method_id if payment_method_id else None, payment_date=timezone.now().date(), notes='Partial Payment (Edit)' ) purchase.save() return JsonResponse({'success': True, 'purchase_id': purchase.id}) except Purchase.DoesNotExist: return JsonResponse({'success': False, 'error': 'Purchase not found'}) except Product.DoesNotExist: return JsonResponse({'success': False, 'error': 'Product not found'}) except Exception as e: return JsonResponse({'success': False, 'error': str(e)})