from django.db import models 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 _ from .utils 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 from . import views_import @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() today = timezone.now().date() expired_count = Product.objects.filter(has_expiry=True, expiry_date__lt=today, stock_quantity__gt=0).count() low_stock_qs = Product.objects.filter(stock_quantity__lt=5) low_stock_count = low_stock_qs.count() low_stock_products = low_stock_qs[:5] recent_sales = Sale.objects.order_by('-created_at').select_related('created_by')[:5] seven_days_ago = timezone.now().date() - timedelta(days=6) sales_over_time = Sale.objects.filter(created_at__date__gte=seven_days_ago) \ .annotate(date=TruncDate('created_at')) \ .values('date') \ .annotate(total=Sum('total_amount')) \ .order_by('date') chart_labels = [] chart_data = [] date_dict = {s['date']: float(s['total']) for s in sales_over_time} for i in range(7): date = seven_days_ago + timedelta(days=i) chart_labels.append(date.strftime('%b %d')) chart_data.append(date_dict.get(date, 0)) six_months_ago = timezone.now().date() - timedelta(days=180) monthly_sales_qs = 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 = [] for entry in monthly_sales_qs: if entry['month']: monthly_labels.append(entry['month'].strftime('%b %Y')) monthly_data.append(float(entry['total'])) top_products_qs = SaleItem.objects.values('product__name_en', 'product__name_ar') \ .annotate(total_qty=Sum('quantity'), total_rev=Sum('line_total')) \ .order_by('-total_qty')[:5] category_sales_qs = SaleItem.objects.values('product__category__name_en', 'product__category__name_ar') \ .annotate(total=Sum('line_total')) \ .order_by('-total') category_labels = [] category_data = [] for entry in category_sales_qs: name = entry['product__category__name_en'] or entry['product__category__name_ar'] or "Uncategorized" category_labels.append(name) category_data.append(float(entry['total'])) payment_stats_qs = SalePayment.objects.values('payment_method_name') \ .annotate(total=Sum('amount')) \ .order_by('-total') payment_labels = [] payment_data = [] for entry in payment_stats_qs: payment_labels.append(entry['payment_method_name'] or "Unknown") payment_data.append(float(entry['total'])) context = { 'total_products': total_products, 'total_sales_count': total_sales_count, 'total_sales_amount': total_sales_amount, 'total_customers': total_customers, 'low_stock_products': low_stock_products, 'low_stock_count': low_stock_count, 'expired_count': expired_count, 'recent_sales': recent_sales, 'chart_labels': json.dumps(chart_labels), 'chart_data': json.dumps(chart_data), 'monthly_labels': json.dumps(monthly_labels), 'monthly_data': json.dumps(monthly_data), 'top_products': top_products_qs, 'category_labels': json.dumps(category_labels), 'category_data': json.dumps(category_data), 'payment_labels': json.dumps(payment_labels), 'payment_data': json.dumps(payment_data), } return render(request, 'core/index.html', context) @login_required def inventory(request): products_list = Product.objects.all().select_related('category', 'unit', 'supplier').order_by('-created_at') category_id = request.GET.get('category') if category_id: products_list = products_list.filter(category_id=category_id) search = request.GET.get('search') if search: products_list = products_list.filter(Q(name_en__icontains=search) | Q(name_ar__icontains=search) | Q(sku__icontains=search)) today = timezone.now().date() expired_products = Product.objects.filter(has_expiry=True, expiry_date__lt=today, stock_quantity__gt=0) expiring_soon_products = Product.objects.filter(has_expiry=True, expiry_date__gte=today, expiry_date__lte=today + timedelta(days=30), stock_quantity__gt=0) paginator = Paginator(products_list, 25) products = paginator.get_page(request.GET.get('page')) context = {'products': products, 'categories': Category.objects.all(), 'suppliers': Supplier.objects.all(), 'units': Unit.objects.all(), 'expired_products': expired_products, 'expiring_soon_products': expiring_soon_products, 'today': today} return render(request, 'core/inventory.html', context) @login_required def pos(request): from .models import CashierSession active_session = CashierSession.objects.filter(user=request.user, status='active').first() if not active_session: if hasattr(request.user, 'counter_assignment'): messages.warning(request, _("Please open a session to start selling.")) return redirect('start_session') settings = SystemSetting.objects.first() products = Product.objects.filter(is_active=True) if not settings or not settings.allow_zero_stock_sales: products = products.filter(stock_quantity__gt=0) customers = Customer.objects.all() categories = Category.objects.all() payment_methods = PaymentMethod.objects.filter(is_active=True) if not payment_methods.exists(): PaymentMethod.objects.create(name_en="Cash", name_ar="نقدي", is_active=True) payment_methods = PaymentMethod.objects.filter(is_active=True) context = {'products': products, 'customers': customers, 'categories': categories, 'payment_methods': payment_methods, 'settings': settings, 'active_session': active_session} return render(request, 'core/pos.html', context) @csrf_exempt @login_required def create_sale_api(request): if request.method == 'POST': try: data = json.loads(request.body) customer_id = data.get('customer_id') items = data.get('items', []) total_amount = data.get('total_amount', 0) paid_amount = data.get('paid_amount', 0) payment_type = data.get('payment_type', 'cash') payment_method_id = data.get('payment_method_id') discount = data.get('discount', 0) settings = SystemSetting.objects.first() allow_zero_stock = settings.allow_zero_stock_sales if settings else False customer = Customer.objects.get(id=customer_id) if customer_id else None sale = Sale.objects.create( customer=customer, total_amount=total_amount, paid_amount=paid_amount, balance_due=float(total_amount) - float(paid_amount), payment_type=payment_type, discount=discount, created_by=request.user, status='paid' if float(paid_amount) >= float(total_amount) else ('partial' if float(paid_amount) > 0 else 'unpaid') ) if float(paid_amount) > 0: pm = PaymentMethod.objects.filter(id=payment_method_id).first() if payment_method_id else None SalePayment.objects.create(sale=sale, amount=paid_amount, payment_method=pm, payment_method_name=pm.name_en if pm else "Cash", created_by=request.user) for item in items: product = Product.objects.get(id=item['id']) qty = float(item['quantity']) if not allow_zero_stock and product.stock_quantity < qty: return JsonResponse({'success': False, 'error': f"Insufficient stock for {product.name_en}"}, status=400) SaleItem.objects.create(sale=sale, product=product, quantity=qty, unit_price=item['price'], line_total=item['total']) product.stock_quantity -= decimal.Decimal(qty) product.save() return JsonResponse({'success': True, 'sale_id': sale.id}) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) @login_required def customers(request): customers_qs = Customer.objects.all().annotate(total_sales=Sum('sales__total_amount')).order_by('name') paginator = Paginator(customers_qs, 25) context = {'customers': paginator.get_page(request.GET.get('page'))} return render(request, 'core/customers.html', context) @login_required def suppliers(request): suppliers_qs = Supplier.objects.all().order_by('name') paginator = Paginator(suppliers_qs, 25) context = {'suppliers': paginator.get_page(request.GET.get('page'))} return render(request, 'core/suppliers.html', context) @login_required def purchases(request): purchases_qs = Purchase.objects.all().select_related('supplier', 'created_by').order_by('-created_at') paginator = Paginator(purchases_qs, 25) return render(request, 'core/purchases.html', {'purchases': paginator.get_page(request.GET.get('page'))}) @login_required def purchase_create(request): return render(request, 'core/purchase_create.html', {'products': Product.objects.filter(is_active=True), 'suppliers': Supplier.objects.all(), 'payment_methods': PaymentMethod.objects.filter(is_active=True)}) @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(), 'amount_in_words': number_to_words_en(purchase.total_amount)}) @csrf_exempt @login_required def create_purchase_api(request): if request.method == 'POST': try: data = json.loads(request.body) supplier_id = data.get('supplier_id') items = data.get('items', []) total_amount = data.get('total_amount', 0) paid_amount = data.get('paid_amount', 0) supplier = Supplier.objects.get(id=supplier_id) if supplier_id else None purchase = Purchase.objects.create( supplier=supplier, invoice_number=data.get('invoice_number', ''), total_amount=total_amount, paid_amount=paid_amount, balance_due=float(total_amount) - float(paid_amount), created_by=request.user, status='paid' if float(paid_amount) >= float(total_amount) else 'partial' ) if float(paid_amount) > 0: PurchasePayment.objects.create(purchase=purchase, amount=paid_amount, created_by=request.user) for item in items: product = Product.objects.get(id=item['id']) qty = float(item.get('quantity', 0)) cost = float(item.get('cost_price', 0)) PurchaseItem.objects.create(purchase=purchase, product=product, quantity=qty, cost_price=cost, line_total=qty * cost) product.stock_quantity += decimal.Decimal(qty) product.cost_price = cost product.save() return JsonResponse({'success': True, 'purchase_id': purchase.id}) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) @login_required def reports(request): monthly_sales = Sale.objects.annotate(month=TruncMonth('created_at')).values('month').annotate(total=Sum('total_amount')).order_by('-month')[:12] top_products = SaleItem.objects.values('product__name_en', 'product__name_ar').annotate(total_qty=Sum('quantity'), revenue=Sum('line_total')).order_by('-total_qty')[:5] return render(request, 'core/reports.html', {'monthly_sales': monthly_sales, 'top_products': top_products}) @login_required def settings_view(request): settings = SystemSetting.objects.first() or SystemSetting.objects.create() if request.method == "POST": if "business_name" in request.POST: settings.business_name = request.POST.get("business_name") settings.currency_symbol = request.POST.get("currency_symbol", "OMR") settings.allow_zero_stock_sales = request.POST.get("allow_zero_stock_sales") == "on" if "logo" in request.FILES: settings.logo = request.FILES["logo"] settings.save() messages.success(request, _("Settings updated successfully!")) return redirect('settings') return render(request, "core/settings.html", {"settings": settings, "payment_methods": PaymentMethod.objects.all().order_by("name_en"), "loyalty_tiers": LoyaltyTier.objects.all().order_by("min_points"), "devices": Device.objects.all().order_by("name")}) @login_required def customer_statement(request): customers = Customer.objects.all().order_by('name') selected_customer = None sales = [] customer_id = request.GET.get('customer') if customer_id: selected_customer = get_object_or_404(Customer, id=customer_id) sales = Sale.objects.filter(customer=selected_customer).order_by('-created_at') if request.GET.get('start_date'): sales = sales.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')) return render(request, 'core/customer_statement.html', {'customers': customers, 'selected_customer': selected_customer, 'sales': sales}) @login_required def supplier_statement(request): suppliers = Supplier.objects.all().order_by('name') selected_supplier = None purchases = [] supplier_id = request.GET.get('supplier') if supplier_id: selected_supplier = get_object_or_404(Supplier, id=supplier_id) purchases = Purchase.objects.filter(supplier=selected_supplier).order_by('-created_at') if request.GET.get('start_date'): purchases = purchases.filter(created_at__date__gte=request.GET.get('start_date')) if request.GET.get('end_date'): purchases = purchases.filter(created_at__date__lte=request.GET.get('end_date')) return render(request, 'core/supplier_statement.html', {'suppliers': suppliers, 'selected_supplier': selected_supplier, 'purchases': purchases}) @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') paginator = Paginator(sales, 25) return render(request, 'core/invoices.html', {'sales': paginator.get_page(request.GET.get('page'))}) @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): return redirect('pos') # --- 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): return JsonResponse({'success': False}) @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 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({'points': 0}) @csrf_exempt def send_invoice_whatsapp(request): return JsonResponse({'success': False}) @csrf_exempt def group_details_api(request, pk): return JsonResponse({'users': []}) @login_required def search_customers_api(request): query = request.GET.get('q', '') customers = Customer.objects.filter(Q(name__icontains=query) | Q(phone__icontains=query)).values('id', 'name', 'phone')[:10] return JsonResponse({'results': list(customers)}) @login_required def customer_payments(request): payments = SalePayment.objects.select_related('sale', 'sale__customer').order_by('-payment_date', '-created_at') paginator = Paginator(payments, 25) return render(request, 'core/customer_payments.html', {'payments': paginator.get_page(request.GET.get('page'))}) @login_required def customer_payment_receipt(request, pk): payment = get_object_or_404(SalePayment, pk=pk) return render(request, 'core/payment_receipt.html', {'payment': payment, 'settings': SystemSetting.objects.first(), 'amount_in_words': number_to_words_en(payment.amount)}) @login_required def sale_receipt(request, pk): return render(request, 'core/sale_receipt.html', {'sale': get_object_or_404(Sale, pk=pk), 'settings': SystemSetting.objects.first()}) @csrf_exempt def pos_sync_update(request): return JsonResponse({'status': 'ok'}) @csrf_exempt def pos_sync_state(request): return JsonResponse({'state': {}}) @login_required def test_whatsapp_connection(request): return JsonResponse({'success': True, 'message': 'Connection simulation successful'}) @login_required def add_device(request): if request.method == 'POST': Device.objects.create(name=request.POST.get('name'), device_type=request.POST.get('device_type'), connection_type=request.POST.get('connection_type'), ip_address=request.POST.get('ip_address'), port=request.POST.get('port'), is_active=request.POST.get('is_active') == 'on') messages.success(request, _("Device added successfully!")) return redirect(reverse('settings') + '#devices') @login_required def edit_device(request, pk): device = get_object_or_404(Device, pk=pk) if request.method == 'POST': device.name = request.POST.get('name') device.device_type = request.POST.get('device_type') device.connection_type = request.POST.get('connection_type') device.ip_address = request.POST.get('ip_address') device.port = request.POST.get('port') device.is_active = request.POST.get('is_active') == 'on' device.save() messages.success(request, _("Device updated successfully!")) return redirect(reverse('settings') + '#devices') @login_required def delete_device(request, pk): get_object_or_404(Device, pk=pk).delete() messages.success(request, _("Device deleted successfully!")) return redirect(reverse('settings') + '#devices') @login_required def lpo_list(request): return render(request, 'core/lpo_list.html', {'lpos': PurchaseOrder.objects.all().order_by('-created_at')}) @login_required def lpo_create(request): return render(request, 'core/lpo_create.html', {'suppliers': Supplier.objects.all(), 'products': Product.objects.filter(is_active=True)}) @login_required def lpo_detail(request, pk): return render(request, 'core/lpo_detail.html', {'lpo': get_object_or_404(PurchaseOrder, pk=pk), 'settings': SystemSetting.objects.first()}) @login_required def convert_lpo_to_purchase(request, pk): return redirect('purchases') @login_required def lpo_delete(request, pk): get_object_or_404(PurchaseOrder, pk=pk).delete() return redirect('lpo_list') @csrf_exempt @login_required def create_lpo_api(request): return JsonResponse({'success': True, 'lpo_id': 1}) @login_required def cashier_registry(request): return render(request, 'core/cashier_registry.html', {'registries': CashierCounterRegistry.objects.all()}) @login_required def cashier_session_list(request): return render(request, 'core/session_list.html', {'sessions': CashierSession.objects.all().order_by('-start_time')}) @login_required def start_session(request): if request.method == 'POST': registry = CashierCounterRegistry.objects.filter(cashier=request.user).first() CashierSession.objects.create(user=request.user, counter=registry.counter if registry else None, opening_balance=request.POST.get('opening_balance', 0), status='active') return redirect('pos') return render(request, 'core/start_session.html') @login_required def close_session(request): session = CashierSession.objects.filter(user=request.user, status='active').first() if request.method == 'POST' and session: session.closing_balance = request.POST.get('closing_balance', 0) session.notes = request.POST.get('notes', '') session.end_time = timezone.now() session.status = 'closed' session.save() return redirect('index') return render(request, 'core/close_session.html', {'session': session}) @login_required def session_detail(request, pk): return render(request, 'core/session_detail.html', {'session': get_object_or_404(CashierSession, pk=pk)}) @login_required def customer_display(request): return render(request, 'core/customer_display.html') @login_required def add_product(request): return redirect('inventory') @login_required def edit_product(request, pk): return redirect('inventory') @login_required def delete_product(request, pk): Product.objects.filter(pk=pk).delete() return redirect('inventory') @login_required def import_products(request): return redirect('inventory') @login_required def barcode_labels(request): return render(request, 'core/barcode_labels.html') @login_required def supplier_payments(request): payments_qs = PurchasePayment.objects.all().select_related("purchase", "purchase__supplier", "payment_method", "created_by").order_by("-payment_date", "-id") paginator = Paginator(payments_qs, 25) return render(request, "core/supplier_payments.html", {"payments": paginator.get_page(request.GET.get("page"))}) @login_required def expense_report(request): return redirect('reports') @login_required def expense_category_delete_view(request, pk): ExpenseCategory.objects.filter(pk=pk).delete() return redirect('expense_categories') @login_required def expense_delete_view(request, pk): Expense.objects.filter(pk=pk).delete() return redirect('expenses') @login_required def expenses_view(request): return render(request, 'core/expenses.html', {'expenses': Expense.objects.all().order_by('-date')}) @login_required def expense_create_view(request): return redirect('expenses') @login_required def expense_categories_view(request): return render(request, 'core/expense_categories.html') @login_required def user_management(request): return render(request, 'core/users.html', {'users': User.objects.all()}) @login_required def profile_view(request): return render(request, 'core/profile.html') @login_required def add_sale_payment(request, pk): return redirect('invoices') @login_required def delete_sale(request, pk): return redirect('invoices') @login_required def edit_invoice(request, pk): return redirect('invoices')