import base64 import os from django.conf import settings as django_settings from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy 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 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) import json from datetime import timedelta from django.utils import timezone from django.contrib import messages from django.utils.text import slugify import openpyxl @login_required def index(request): """ Enhanced Meezan Dashboard View """ # Summary Stats 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() # Expired Items Alert today = timezone.now().date() expired_count = Product.objects.filter(has_expiry=True, expiry_date__lt=today, stock_quantity__gt=0).count() # Stock Alert (Low stock < 5) 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 Transactions recent_sales = Sale.objects.order_by('-created_at').select_related('created_by')[:5] # Chart Data: Sales for the last 7 days 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') # Prepare data for Chart.js 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)) 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), } 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') # Filter by category category_id = request.GET.get('category') if category_id: products_list = products_list.filter(category_id=category_id) # Search 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) ) # Expired items 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) page_number = request.GET.get('page') products = paginator.get_page(page_number) categories = Category.objects.all() suppliers = Supplier.objects.all() units = Unit.objects.all() context = { 'products': products, 'categories': categories, 'suppliers': suppliers, 'units': units, '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 # Check for active session active_session = CashierSession.objects.filter(user=request.user, status='active').first() if not active_session: # Check if user is a cashier (assigned to a counter) if hasattr(request.user, 'counter_assignment'): messages.warning(request, _("Please open a session to start selling.")) return redirect('start_session') products = Product.objects.all().filter(stock_quantity__gt=0, is_active=True) customers = Customer.objects.all() categories = Category.objects.all() payment_methods = PaymentMethod.objects.filter(is_active=True) settings = SystemSetting.objects.first() # Ensure at least Cash exists 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) @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) page_number = request.GET.get('page') customers_list = paginator.get_page(page_number) context = {'customers': customers_list} 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) page_number = request.GET.get('page') suppliers_list = paginator.get_page(page_number) context = {'suppliers': suppliers_list} return render(request, 'core/suppliers.html', context) # --- Purchase Views --- @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) page_number = request.GET.get("page") payments = paginator.get_page(page_number) return render(request, "core/supplier_payments.html", {"payments": payments}) def purchases(request): purchases_qs = Purchase.objects.all().select_related('supplier', 'created_by').order_by('-created_at') paginator = Paginator(purchases_qs, 25) page_number = request.GET.get('page') purchases_list = paginator.get_page(page_number) suppliers_qs = Supplier.objects.all().order_by('name') paginator = Paginator(suppliers_qs, 25) page_number = request.GET.get('page') suppliers_list = paginator.get_page(page_number) payment_methods = PaymentMethod.objects.filter(is_active=True) context = { 'purchases': purchases_list, 'suppliers': suppliers_list, 'payment_methods': payment_methods } return render(request, 'core/purchases.html', context) @login_required def purchase_create(request): products = Product.objects.filter(is_active=True) suppliers = Supplier.objects.all() payment_methods = PaymentMethod.objects.filter(is_active=True) return render(request, 'core/purchase_create.html', { 'products': products, 'suppliers': suppliers, 'payment_methods': payment_methods }) @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, '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') invoice_number = data.get('invoice_number', '') 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') due_date = data.get('due_date') notes = data.get('notes', '') supplier = None if supplier_id: supplier = Supplier.objects.get(id=supplier_id) purchase = Purchase.objects.create( supplier=supplier, invoice_number=invoice_number, total_amount=total_amount, paid_amount=paid_amount, balance_due=float(total_amount) - float(paid_amount), payment_type=payment_type, due_date=due_date if due_date else None, notes=notes, created_by=request.user ) # Set status based on payments if float(paid_amount) >= float(total_amount): purchase.status = 'paid' elif float(paid_amount) > 0: purchase.status = 'partial' else: purchase.status = 'unpaid' purchase.save() # Record the initial payment if any if float(paid_amount) > 0: pm = None if payment_method_id: pm = PaymentMethod.objects.filter(id=payment_method_id).first() PurchasePayment.objects.create( purchase=purchase, amount=paid_amount, payment_method=pm, payment_method_name=pm.name_en if pm else payment_type.capitalize(), notes="Initial payment", created_by=request.user ) for item in items: product = Product.objects.get(id=item['id']) item_expiry = item.get('expiry_date') PurchaseItem.objects.create( purchase=purchase, product=product, quantity=item['quantity'], cost_price=item['price'], expiry_date=item_expiry if item_expiry else None, line_total=item['line_total'] ) # Update Stock product.stock_quantity += int(item['quantity']) product.cost_price = item['price'] if item_expiry: product.has_expiry = True if not product.expiry_date or str(item_expiry) > str(product.expiry_date): product.expiry_date = item_expiry 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 add_purchase_payment(request, pk): purchase = get_object_or_404(Purchase, pk=pk) if request.method == 'POST': amount = request.POST.get('amount') payment_date = request.POST.get('payment_date', timezone.now().date()) payment_method_id = request.POST.get('payment_method_id') notes = request.POST.get('notes', '') pm = None if payment_method_id: pm = PaymentMethod.objects.filter(id=payment_method_id).first() PurchasePayment.objects.create( purchase=purchase, amount=amount, payment_date=payment_date, payment_method=pm, payment_method_name=pm.name_en if pm else "Cash", notes=notes, created_by=request.user ) purchase.update_balance() messages.success(request, _("Payment added successfully!")) return redirect('purchases') @login_required def delete_purchase(request, pk): purchase = get_object_or_404(Purchase, pk=pk) for item in purchase.items.all(): item.product.stock_quantity -= item.quantity item.product.save() purchase.delete() messages.success(request, _("Purchase deleted successfully!")) return redirect('purchases') # --- Sale Views --- @login_required def invoice_list(request): sales = Sale.objects.all().select_related("customer", "created_by") # Filtering start_date = request.GET.get("start_date") end_date = request.GET.get("end_date") customer_id = request.GET.get("customer") status = request.GET.get("status") if start_date: sales = sales.filter(created_at__date__gte=start_date) if end_date: sales = sales.filter(created_at__date__lte=end_date) if customer_id: sales = sales.filter(customer_id=customer_id) if status: sales = sales.filter(status=status) sales = sales.order_by("-created_at") paginator = Paginator(sales, 25) page_number = request.GET.get("page") sales = paginator.get_page(page_number) customers = Customer.objects.all() payment_methods = PaymentMethod.objects.filter(is_active=True) return render(request, "core/invoices.html", { "sales": sales, "customers": customers, "payment_methods": payment_methods }) @login_required def invoice_create(request): products = Product.objects.filter(is_active=True) customers = Customer.objects.all() payment_methods = PaymentMethod.objects.filter(is_active=True) return render(request, 'core/invoice_create.html', { 'products': products, 'customers': customers, 'payment_methods': payment_methods }) @login_required def invoice_detail(request, pk): sale = get_object_or_404(Sale, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/invoice_detail.html', { 'sale': sale, 'settings': settings, 'amount_in_words': number_to_words_en(sale.total_amount) }) @login_required def edit_invoice(request, pk): sale = get_object_or_404(Sale, pk=pk) # Prepare cart items for JSON cart_items = [] for item in sale.items.all(): cart_items.append({ 'id': item.product.id, 'name_en': item.product.name_en, 'sku': item.product.sku, 'price': float(item.unit_price), 'quantity': item.quantity }) customers = Customer.objects.all() products = Product.objects.filter(is_active=True) payment_methods = PaymentMethod.objects.filter(is_active=True) # Find initial payment method initial_payment = sale.payments.filter(notes='Initial payment').first() payment_method_id = initial_payment.payment_method_id if initial_payment else '' return render(request, 'core/invoice_edit.html', { 'sale': sale, 'customers': customers, 'products': products, 'payment_methods': payment_methods, 'cart_json': json.dumps(cart_items), 'payment_method_id': payment_method_id }) @csrf_exempt def create_sale_api(request): if request.method == 'POST': try: data = json.loads(request.body) customer_id = data.get('customer_id') invoice_number = data.get('invoice_number', '') items = data.get('items', []) # Retrieve amounts subtotal = data.get('subtotal', 0) vat_amount = data.get('vat_amount', 0) total_amount = data.get('total_amount', 0) paid_amount = data.get('paid_amount', 0) discount = data.get('discount', 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', '') # Loyalty data points_to_redeem = data.get('loyalty_points_redeemed', 0) customer = None if customer_id: customer = Customer.objects.get(id=customer_id) if not customer and payment_type != 'cash': return JsonResponse({'success': False, 'error': _('Credit or Partial payments are not allowed for Guest customers.')}, status=400) settings = SystemSetting.objects.first() if not settings: settings = SystemSetting.objects.create() # Check for stock availability if overselling is not allowed if not settings.allow_zero_stock_sales: for item in items: try: product = Product.objects.get(id=item["id"]) if product.stock_quantity < float(item["quantity"]): return JsonResponse({"success": False, "error": _("Insufficient stock for product: ") + product.name_en}, status=400) except Product.DoesNotExist: pass loyalty_discount = 0 if settings.loyalty_enabled and customer and points_to_redeem > 0: if customer.loyalty_points >= points_to_redeem: loyalty_discount = float(points_to_redeem) * float(settings.currency_per_point) sale = Sale.objects.create( customer=customer, invoice_number=invoice_number, subtotal=subtotal, vat_amount=vat_amount, total_amount=total_amount, paid_amount=paid_amount, balance_due=float(total_amount) - float(paid_amount), discount=discount, loyalty_points_redeemed=points_to_redeem, loyalty_discount_amount=loyalty_discount, payment_type=payment_type, due_date=due_date if due_date else None, notes=notes, created_by=request.user ) # Set status based on payments if float(paid_amount) >= float(total_amount): sale.status = 'paid' elif float(paid_amount) > 0: sale.status = 'partial' else: sale.status = 'unpaid' sale.save() # Record initial payment if any if float(paid_amount) > 0: pm = None if payment_method_id: pm = PaymentMethod.objects.filter(id=payment_method_id).first() SalePayment.objects.create( sale=sale, amount=paid_amount, payment_method=pm, payment_method_name=pm.name_en if pm else payment_type.capitalize(), notes="Initial payment", created_by=request.user ) for item in items: product = Product.objects.get(id=item['id']) SaleItem.objects.create( sale=sale, product=product, quantity=item['quantity'], unit_price=item['price'], line_total=item['line_total'] ) product.stock_quantity -= int(item['quantity']) product.save() # Handle Loyalty Points if settings.loyalty_enabled and customer: # Earn Points points_earned = float(total_amount) * float(settings.points_per_currency) if customer.loyalty_tier: points_earned *= float(customer.loyalty_tier.point_multiplier) if points_earned > 0: customer.loyalty_points += decimal.Decimal(str(points_earned)) LoyaltyTransaction.objects.create( customer=customer, sale=sale, transaction_type='earned', points=points_earned, notes=f"Points earned from Sale #{sale.id}" ) # Redeem Points if points_to_redeem > 0: customer.loyalty_points -= decimal.Decimal(str(points_to_redeem)) LoyaltyTransaction.objects.create( customer=customer, sale=sale, transaction_type='redeemed', points=-points_to_redeem, notes=f"Points redeemed for Sale #{sale.id}" ) customer.update_tier() customer.save() return JsonResponse({ 'success': True, 'sale_id': sale.id, 'business': { 'name': settings.business_name, 'address': settings.address, 'phone': settings.phone, 'email': settings.email, 'currency': settings.currency_symbol, 'vat_number': settings.vat_number, 'registration_number': settings.registration_number, 'logo_url': settings.logo.url if settings.logo else None }, 'sale': { 'id': sale.id, 'invoice_number': sale.invoice_number, 'created_at': sale.created_at.strftime("%Y-%m-%d %H:%M"), 'subtotal': float(sale.subtotal), 'vat_amount': float(sale.vat_amount), 'total': float(sale.total_amount), 'discount': float(sale.discount), 'paid': float(sale.paid_amount), 'balance': float(sale.balance_due), 'customer_name': sale.customer.name if sale.customer else 'Guest', 'items': [ { 'name_en': si.product.name_en, 'name_ar': si.product.name_ar, 'qty': si.quantity, 'price': float(si.unit_price), 'total': float(si.line_total) } for si in sale.items.all() ] } }) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}, status=400) def send_invoice_whatsapp(request): if request.method == 'POST': try: data = json.loads(request.body) sale_id = data.get('sale_id') phone = data.get('phone') pdf_base64 = data.get('pdf_data') if not phone or not pdf_base64: return JsonResponse({'success': False, 'error': 'Missing phone or PDF data.'}, status=400) if ',' in pdf_base64: pdf_base64 = pdf_base64.split(',')[1] pdf_content = base64.b64decode(pdf_base64) temp_dir = os.path.join(django_settings.MEDIA_ROOT, 'temp_invoices') if not os.path.exists(temp_dir): os.makedirs(temp_dir) filename = f'Invoice_{sale_id}.pdf' file_path_pdf = os.path.join(temp_dir, filename) with open(file_path_pdf, 'wb') as f_pdf: f_pdf.write(pdf_content) base_url = request.build_absolute_uri('/') document_url = f"{base_url.rstrip('/')}{django_settings.MEDIA_URL}temp_invoices/{filename}" sale = Sale.objects.filter(id=sale_id).first() invoice_num = sale.invoice_number if sale and sale.invoice_number else sale_id caption = f'Invoice #{invoice_num}' success, message = send_whatsapp_document(phone, document_url, caption) return JsonResponse({'success': success, 'message': message}) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}, status=500) return JsonResponse({'success': False, 'error': 'Invalid request method.'}, status=405) @login_required def add_sale_payment(request, pk): sale = get_object_or_404(Sale, pk=pk) if request.method == 'POST': amount = request.POST.get('amount') payment_date = request.POST.get('payment_date', timezone.now().date()) payment_method_id = request.POST.get('payment_method_id') notes = request.POST.get('notes', '') pm = None if payment_method_id: pm = PaymentMethod.objects.filter(id=payment_method_id).first() SalePayment.objects.create( sale=sale, amount=amount, payment_date=payment_date, payment_method=pm, payment_method_name=pm.name_en if pm else "Cash", notes=notes, created_by=request.user ) sale.update_balance() messages.success(request, _("Payment added successfully!")) return redirect('invoices') @login_required def delete_sale(request, pk): sale = get_object_or_404(Sale, pk=pk) for item in sale.items.all(): item.product.stock_quantity += item.quantity item.product.save() sale.delete() messages.success(request, _("Sale deleted successfully!")) return redirect('invoices') # --- Quotation Views --- @login_required def quotations(request): quotations_qs = Quotation.objects.all().select_related('customer', 'created_by').order_by('-created_at') paginator = Paginator(quotations_qs, 25) page_number = request.GET.get('page') quotations_list = paginator.get_page(page_number) customers = Customer.objects.all() return render(request, 'core/quotations.html', {'quotations': quotations_list, 'customers': customers}) @login_required def quotation_create(request): products = Product.objects.filter(is_active=True) customers = Customer.objects.all() return render(request, 'core/quotation_create.html', {'products': products, 'customers': customers}) @login_required def quotation_detail(request, pk): quotation = get_object_or_404(Quotation, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/quotation_detail.html', { 'quotation': quotation, 'settings': settings, 'amount_in_words': number_to_words_en(quotation.total_amount) }) @csrf_exempt @login_required def create_quotation_api(request): if request.method == 'POST': try: data = json.loads(request.body) customer_id = data.get('customer_id') quotation_number = data.get('quotation_number', '') items = data.get('items', []) total_amount = data.get('total_amount', 0) discount = data.get('discount', 0) valid_until = data.get('valid_until') terms_and_conditions = data.get('terms_and_conditions', '') notes = data.get('notes', '') customer = None if customer_id: customer = Customer.objects.get(id=customer_id) quotation = Quotation.objects.create( customer=customer, quotation_number=quotation_number, total_amount=total_amount, discount=discount, valid_until=valid_until if valid_until else None, terms_and_conditions=terms_and_conditions, notes=notes, created_by=request.user ) for item in items: product = Product.objects.get(id=item['id']) QuotationItem.objects.create( quotation=quotation, product=product, quantity=item['quantity'], unit_price=item['price'], line_total=item['line_total'] ) return JsonResponse({'success': True, 'quotation_id': quotation.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 convert_quotation_to_invoice(request, pk): quotation = get_object_or_404(Quotation, pk=pk) if quotation.status == 'converted': messages.warning(request, _("This quotation has already been converted to an invoice.")) return redirect('invoices') # Check stock before converting settings = SystemSetting.objects.first() or SystemSetting.objects.create() if not settings.allow_zero_stock_sales: for item in quotation.items.all(): if item.product.stock_quantity < item.quantity: messages.error(request, _("Insufficient stock for product: ") + item.product.name_en) return redirect('quotation_detail', pk=pk) # Create Sale from Quotation sale = Sale.objects.create( customer=quotation.customer, quotation=quotation, total_amount=quotation.total_amount, discount=quotation.discount, balance_due=quotation.total_amount, payment_type='cash', status='unpaid', notes=quotation.notes, created_by=request.user ) # Create SaleItems and Update Stock for item in quotation.items.all(): SaleItem.objects.create( sale=sale, product=item.product, quantity=item.quantity, unit_price=item.unit_price, line_total=item.line_total ) # Deduct Stock item.product.stock_quantity -= item.quantity item.product.save() # Update Quotation Status quotation.status = 'converted' quotation.save() messages.success(request, _("Quotation converted to Invoice successfully!")) return redirect('invoice_detail', pk=sale.pk) @login_required def delete_quotation(request, pk): quotation = get_object_or_404(Quotation, pk=pk) quotation.delete() messages.success(request, _("Quotation deleted successfully!")) return redirect('quotations') # --- Sale Return Views --- @login_required def sales_returns(request): returns_qs = SaleReturn.objects.all().select_related('customer', 'created_by').order_by('-created_at') paginator = Paginator(returns_qs, 25) page_number = request.GET.get('page') returns = paginator.get_page(page_number) return render(request, 'core/sales_returns.html', {'returns': returns}) @login_required def sale_return_create(request): products = Product.objects.filter(is_active=True) customers = Customer.objects.all() sales = Sale.objects.all().order_by('-created_at') return render(request, 'core/sale_return_create.html', { 'products': products, 'customers': customers, 'sales': sales }) @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, 'amount_in_words': number_to_words_en(sale_return.total_amount) }) @csrf_exempt @login_required def create_sale_return_api(request): if request.method == 'POST': try: data = json.loads(request.body) sale_id = data.get('sale_id') customer_id = data.get('customer_id') return_number = data.get('return_number', '') items = data.get('items', []) total_amount = data.get('total_amount', 0) notes = data.get('notes', '') customer = None if customer_id: customer = Customer.objects.get(id=customer_id) sale = None if sale_id: sale = Sale.objects.get(id=sale_id) sale_return = SaleReturn.objects.create( sale=sale, customer=customer, return_number=return_number, total_amount=total_amount, notes=notes, created_by=request.user ) for item in items: product = Product.objects.get(id=item['id']) SaleReturnItem.objects.create( sale_return=sale_return, product=product, quantity=item['quantity'], unit_price=item['price'], line_total=item['line_total'] ) # Increase Stock for Sales Return product.stock_quantity += int(item['quantity']) product.save() return JsonResponse({'success': True, 'return_id': sale_return.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 delete_sale_return(request, pk): sale_return = get_object_or_404(SaleReturn, pk=pk) for item in sale_return.items.all(): item.product.stock_quantity -= item.quantity item.product.save() sale_return.delete() messages.success(request, _("Sale return deleted successfully!")) return redirect('sales_returns') # --- Purchase Return Views --- @login_required def purchase_returns(request): returns_qs = PurchaseReturn.objects.all().select_related('supplier', 'created_by').order_by('-created_at') paginator = Paginator(returns_qs, 25) page_number = request.GET.get('page') returns = paginator.get_page(page_number) return render(request, 'core/purchase_returns.html', {'returns': returns}) @login_required def purchase_return_create(request): products = Product.objects.filter(is_active=True) suppliers = Supplier.objects.all() purchases = Purchase.objects.all().order_by('-created_at') return render(request, 'core/purchase_return_create.html', { 'products': products, 'customers': suppliers, 'purchases': purchases }) @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, 'amount_in_words': number_to_words_en(purchase_return.total_amount) }) @csrf_exempt @login_required def create_purchase_return_api(request): if request.method == 'POST': try: data = json.loads(request.body) purchase_id = data.get('purchase_id') supplier_id = data.get('supplier_id') return_number = data.get('return_number', '') items = data.get('items', []) total_amount = data.get('total_amount', 0) notes = data.get('notes', '') supplier = None if supplier_id: supplier = Supplier.objects.get(id=supplier_id) purchase = None if purchase_id: purchase = Purchase.objects.get(id=purchase_id) purchase_return = PurchaseReturn.objects.create( purchase=purchase, supplier=supplier, return_number=return_number, total_amount=total_amount, notes=notes, created_by=request.user ) for item in items: product = Product.objects.get(id=item['id']) PurchaseReturnItem.objects.create( purchase_return=purchase_return, product=product, quantity=item['quantity'], cost_price=item['price'], line_total=item['line_total'] ) # Decrease Stock for Purchase Return product.stock_quantity -= int(item['quantity']) product.save() return JsonResponse({'success': True, 'return_id': purchase_return.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 delete_purchase_return(request, pk): purchase_return = get_object_or_404(PurchaseReturn, pk=pk) for item in purchase_return.items.all(): item.product.stock_quantity += item.quantity item.product.save() purchase_return.delete() messages.success(request, _("Purchase return deleted successfully!")) return redirect('purchase_returns') # --- Other Management Views --- @login_required def reports(request): """ Smart Reports View """ # Monthly Revenue monthly_sales = Sale.objects.annotate(month=TruncMonth('created_at')) \ .values('month') \ .annotate(total=Sum('total_amount')) \ .order_by('-month')[:12] # Top Selling Products 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] context = { 'monthly_sales': monthly_sales, 'top_products': top_products, } return render(request, 'core/reports.html', context) @login_required def settings_view(request): """ Smart Admin Settings View """ settings = SystemSetting.objects.first() if not settings: settings = SystemSetting.objects.create() if request.method == "POST": if "business_name" in request.POST: settings.business_name = request.POST.get("business_name") or "Meezan Accounting" settings.address = request.POST.get("address", "") settings.phone = request.POST.get("phone", "") settings.email = request.POST.get("email", "") 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.vat_number = request.POST.get("vat_number", "") settings.registration_number = request.POST.get("registration_number", "") settings.allow_zero_stock_sales = request.POST.get("allow_zero_stock_sales") == "on" settings.loyalty_enabled = request.POST.get("loyalty_enabled") == "on" settings.points_per_currency = request.POST.get("points_per_currency", 1.0) settings.currency_per_point = request.POST.get("currency_per_point", 0.010) settings.min_points_to_redeem = request.POST.get("min_points_to_redeem", 100) if "logo" in request.FILES: settings.logo = request.FILES["logo"] elif "wablas_token" in request.POST or "wablas_enabled" in request.POST or "wablas_server_url" in request.POST: settings.wablas_enabled = request.POST.get("wablas_enabled") == "on" settings.wablas_token = request.POST.get("wablas_token", "") settings.wablas_server_url = request.POST.get("wablas_server_url", "") settings.wablas_secret_key = request.POST.get("wablas_secret_key", "") settings.save() messages.success(request, _("Settings updated successfully!")) if "business_name" in request.POST: return redirect(reverse("settings") + "#profile") elif "wablas_token" in request.POST or "wablas_enabled" in request.POST or "wablas_server_url" in request.POST: return redirect(reverse("settings") + "#whatsapp") else: return redirect(reverse("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") context = { "settings": settings, "payment_methods": payment_methods, "loyalty_tiers": loyalty_tiers, "devices": devices } return render(request, "core/settings.html", context) @login_required def add_payment_method(request): if request.method == 'POST': name_en = request.POST.get('name_en') name_ar = request.POST.get('name_ar') is_active = request.POST.get('is_active') == 'on' has_expiry = request.POST.get('has_expiry') == 'on' expiry_date = request.POST.get('expiry_date') if not has_expiry: expiry_date = None PaymentMethod.objects.create(name_en=name_en, name_ar=name_ar, is_active=is_active) messages.success(request, _("Payment method added successfully!")) return redirect(reverse('settings') + '#payments') @login_required def edit_payment_method(request, pk): pm = get_object_or_404(PaymentMethod, pk=pk) if request.method == 'POST': pm.name_en = request.POST.get('name_en') pm.name_ar = request.POST.get('name_ar') pm.is_active = request.POST.get('is_active') == 'on' pm.save() messages.success(request, _("Payment method updated successfully!")) return redirect(reverse('settings') + '#payments') @login_required def delete_payment_method(request, pk): pm = get_object_or_404(PaymentMethod, pk=pk) pm.delete() messages.success(request, _("Payment method deleted successfully!")) return redirect(reverse('settings') + '#payments') @login_required def add_customer(request): if request.method == 'POST': name = request.POST.get('name') phone = request.POST.get('phone') email = request.POST.get('email') address = request.POST.get('address') Customer.objects.create(name=name, phone=phone, email=email, address=address) messages.success(request, _("Customer added successfully!")) return redirect('customers') @login_required def edit_customer(request, pk): customer = get_object_or_404(Customer, pk=pk) if request.method == 'POST': customer.name = request.POST.get('name') customer.phone = request.POST.get('phone') customer.email = request.POST.get('email') customer.address = request.POST.get('address') customer.save() messages.success(request, _("Customer updated successfully!")) return redirect('customers') @login_required def delete_customer(request, pk): customer = get_object_or_404(Customer, pk=pk) customer.delete() messages.success(request, _("Customer deleted successfully!")) return redirect('customers') @login_required def add_supplier(request): if request.method == 'POST': name = request.POST.get('name') contact_person = request.POST.get('contact_person') phone = request.POST.get('phone') Supplier.objects.create(name=name, contact_person=contact_person, phone=phone) messages.success(request, _("Supplier added successfully!")) return redirect('suppliers') @login_required def edit_supplier(request, pk): supplier = get_object_or_404(Supplier, pk=pk) if request.method == 'POST': supplier.name = request.POST.get('name') supplier.contact_person = request.POST.get('contact_person') supplier.phone = request.POST.get('phone') supplier.save() messages.success(request, _("Supplier updated successfully!")) return redirect('suppliers') @login_required def delete_supplier(request, pk): supplier = get_object_or_404(Supplier, pk=pk) supplier.delete() messages.success(request, _("Supplier deleted successfully!")) return redirect('suppliers') @login_required def suggest_sku(request): """ API endpoint to suggest a unique SKU. """ while True: # Generate a random 8-digit number sku = "".join(random.choices(string.digits, k=8)) if not Product.objects.filter(sku=sku).exists(): return JsonResponse({"sku": sku}) @login_required def add_product(request): if request.method == 'POST': name_en = request.POST.get('name_en') name_ar = request.POST.get('name_ar') category_id = request.POST.get('category') unit_id = request.POST.get('unit') supplier_id = request.POST.get('supplier') sku = request.POST.get('sku') if not sku: while True: sku = ''.join(random.choices(string.digits, k=8)) if not Product.objects.filter(sku=sku).exists(): break cost_price = request.POST.get('cost_price', 0) price = request.POST.get('price', 0) vat = request.POST.get('vat', 0) description = request.POST.get('description', '') opening_stock = request.POST.get('opening_stock', 0) stock_quantity = request.POST.get('stock_quantity', 0) is_active = request.POST.get('is_active') == 'on' has_expiry = request.POST.get('has_expiry') == 'on' expiry_date = request.POST.get('expiry_date') if not has_expiry: expiry_date = None category = get_object_or_404(Category, id=category_id) unit = get_object_or_404(Unit, id=unit_id) if unit_id else None supplier = get_object_or_404(Supplier, id=supplier_id) if supplier_id else None product = Product.objects.create( name_en=name_en, name_ar=name_ar, category=category, unit=unit, supplier=supplier, sku=sku, cost_price=cost_price, price=price, vat=vat, description=description, opening_stock=opening_stock, stock_quantity=stock_quantity, is_active=is_active, has_expiry=has_expiry, min_stock_level=request.POST.get('min_stock_level', 0), expiry_date=expiry_date ) if 'image' in request.FILES: product.image = request.FILES['image'] product.save() messages.success(request, _("Product added successfully!")) return redirect(reverse('inventory') + '#items') @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.category = get_object_or_404(Category, id=request.POST.get('category')) unit_id = request.POST.get('unit') product.unit = get_object_or_404(Unit, id=unit_id) if unit_id else None supplier_id = request.POST.get('supplier') product.supplier = get_object_or_404(Supplier, id=supplier_id) if supplier_id else None product.cost_price = request.POST.get('cost_price', 0) product.price = request.POST.get('price', 0) product.vat = request.POST.get('vat', 0) product.description = request.POST.get('description', '') product.opening_stock = request.POST.get('opening_stock', 0) product.stock_quantity = request.POST.get('stock_quantity', 0) product.min_stock_level = request.POST.get('min_stock_level', 0) product.is_active = request.POST.get('is_active') == 'on' product.has_expiry = request.POST.get('has_expiry') == 'on' product.expiry_date = request.POST.get('expiry_date') if not product.has_expiry: product.expiry_date = None if 'image' in request.FILES: product.image = request.FILES['image'] product.save() messages.success(request, _("Product updated successfully!")) return redirect(reverse('inventory') + '#items') return redirect(reverse('inventory') + '#items') return redirect(reverse('inventory') + '#items') @login_required def delete_product(request, pk): product = get_object_or_404(Product, pk=pk) product.delete() messages.success(request, _("Product deleted successfully!")) return redirect(reverse('inventory') + '#items') @login_required def add_category(request): if request.method == 'POST': name_en = request.POST.get('name_en') name_ar = request.POST.get('name_ar') slug = slugify(name_en) Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug) messages.success(request, _("Category added successfully!")) return redirect(reverse('inventory') + '#categories-list') @login_required def edit_category(request, pk): category = get_object_or_404(Category, pk=pk) if request.method == 'POST': category.name_en = request.POST.get('name_en') category.name_ar = request.POST.get('name_ar') category.slug = slugify(category.name_en) category.save() messages.success(request, _("Category updated successfully!")) return redirect(reverse('inventory') + '#categories-list') @login_required def delete_category(request, pk): category = get_object_or_404(Category, pk=pk) category.delete() messages.success(request, _("Category deleted successfully!")) return redirect(reverse('inventory') + '#categories-list') @login_required def add_unit(request): if request.method == 'POST': name_en = request.POST.get('name_en') name_ar = request.POST.get('name_ar') short_name = request.POST.get('short_name') Unit.objects.create(name_en=name_en, name_ar=name_ar, short_name=short_name) messages.success(request, _("Unit added successfully!")) return redirect(reverse('inventory') + '#units-list') @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 successfully!")) return redirect(reverse('inventory') + '#units-list') @login_required def delete_unit(request, pk): unit = get_object_or_404(Unit, pk=pk) unit.delete() messages.success(request, _("Unit deleted successfully!")) return redirect(reverse('inventory') + '#units-list') @login_required def barcode_labels(request): products = Product.objects.filter(is_active=True).order_by('name_en') context = {'products': products} return render(request, 'core/barcode_labels.html', context) @login_required def import_products(request): """ Import products from an Excel (.xlsx) file. Expected columns: Name (Eng), Name (Ar), SKU, Cost Price, Sale Price """ if request.method == 'POST' and request.FILES.get('excel_file'): excel_file = request.FILES['excel_file'] if not excel_file.name.endswith('.xlsx'): messages.error(request, _("Please upload a valid .xlsx file.")) return redirect(reverse('inventory') + '#items') try: wb = openpyxl.load_workbook(excel_file) sheet = wb.active # Get or create a default category default_category, _ = Category.objects.get_or_create( name_en="General", defaults={'name_ar': "عام", 'slug': 'general'} ) count = 0 updated_count = 0 errors = [] # Skip header row (min_row=2) for i, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2): if not any(row): continue # Skip empty rows # Unpack columns with fallbacks for safety # Format: name_en, name_ar, sku, cost_price, sale_price name_en = str(row[0]).strip() if row[0] else None name_ar = str(row[1]).strip() if len(row) > 1 and row[1] else name_en sku = str(row[2]).strip() if len(row) > 2 and row[2] else None cost_price = row[3] if len(row) > 3 and row[3] is not None else 0 sale_price = row[4] if len(row) > 4 and row[4] is not None else 0 if not name_en: errors.append(f"Row {i}: Missing English Name. Skipped.") continue if not sku: # Generate unique SKU if missing while True: sku = "".join(random.choices(string.digits, k=8)) if not Product.objects.filter(sku=sku).exists(): break product, created = Product.objects.update_or_create( sku=sku, defaults={ 'name_en': name_en, 'name_ar': name_ar, 'cost_price': cost_price, 'price': sale_price, 'category': default_category, 'is_active': True } ) if created: count += 1 else: updated_count += 1 if count > 0 or updated_count > 0: msg = f"Import completed: {count} new items added" if updated_count > 0: msg += f", {updated_count} items updated" messages.success(request, msg) if errors: for error in errors: messages.warning(request, error) except Exception as e: messages.error(request, f"Error processing file: {str(e)}") return redirect(reverse('inventory') + '#items') @csrf_exempt @login_required def add_category_ajax(request): if request.method == 'POST': try: data = json.loads(request.body) name_en = data.get('name_en') name_ar = data.get('name_ar') if not name_en or not name_ar: return JsonResponse({'success': False, 'error': 'Missing names'}, status=400) slug = slugify(name_en) category = Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug) return JsonResponse({ 'success': True, 'id': category.id, 'name_en': category.name_en, 'name_ar': category.name_ar }) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) @csrf_exempt @login_required def add_unit_ajax(request): if request.method == 'POST': try: data = json.loads(request.body) name_en = data.get('name_en') name_ar = data.get('name_ar') short_name = data.get('short_name') if not name_en or not name_ar or not short_name: return JsonResponse({'success': False, 'error': 'Missing fields'}, status=400) unit = Unit.objects.create(name_en=name_en, name_ar=name_ar, short_name=short_name) return JsonResponse({ 'success': True, 'id': unit.id, 'name_en': unit.name_en, 'name_ar': unit.name_ar }) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) @csrf_exempt @login_required def add_supplier_ajax(request): if request.method == 'POST': try: data = json.loads(request.body) name = data.get('name') contact_person = data.get('contact_person', '') phone = data.get('phone', '') if not name: return JsonResponse({'success': False, 'error': 'Missing name'}, status=400) supplier = Supplier.objects.create(name=name, contact_person=contact_person, phone=phone) return JsonResponse({ 'success': True, 'id': supplier.id, 'name': supplier.name }) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) @login_required def user_management(request): if not (request.user.is_superuser or request.user.groups.filter(name='admin').exists()): messages.error(request, _("Access denied.")) return redirect('index') users_qs = User.objects.all().prefetch_related('groups').order_by('username') paginator = Paginator(users_qs, 25) page_number = request.GET.get('page') users = paginator.get_page(page_number) groups = Group.objects.all().prefetch_related('permissions') # Filter for relevant permissions (core and auth) excluded_apps = ['admin', 'auth', 'contenttypes', 'sessions'] permissions = Permission.objects.select_related('content_type').exclude(content_type__app_label__in=excluded_apps).order_by('content_type__app_label', 'content_type__model', 'codename') if request.method == 'POST': action = request.POST.get('action') if action == 'add': username = request.POST.get('username') password = request.POST.get('password') email = request.POST.get('email') group_ids = request.POST.getlist('groups') if User.objects.filter(username=username).exists(): messages.error(request, _("Username already exists.")) else: user = User.objects.create_user(username=username, email=email, password=password) if group_ids: selected_groups = Group.objects.filter(id__in=group_ids) user.groups.set(selected_groups) user.is_staff = True user.save() messages.success(request, f"User {username} created successfully.") elif action == 'edit_user': user_id = request.POST.get('user_id') user = get_object_or_404(User, id=user_id) user.email = request.POST.get('email') group_ids = request.POST.getlist('groups') selected_groups = Group.objects.filter(id__in=group_ids) user.groups.set(selected_groups) password = request.POST.get('password') if password: user.set_password(password) user.save() messages.success(request, f"User {user.username} updated.") elif action == 'add_group': name = request.POST.get('name') permission_ids = request.POST.getlist('permissions') if Group.objects.filter(name=name).exists(): messages.error(request, _("Group name already exists.")) else: group = Group.objects.create(name=name) if permission_ids: perms = Permission.objects.filter(id__in=permission_ids) group.permissions.set(perms) messages.success(request, f"Group {name} created successfully.") elif action == 'edit_group': group_id = request.POST.get('group_id') group = get_object_or_404(Group, id=group_id) group.name = request.POST.get('name') permission_ids = request.POST.getlist('permissions') perms = Permission.objects.filter(id__in=permission_ids) group.permissions.set(perms) group.save() messages.success(request, f"Group {group.name} updated.") elif action == 'delete_group': group_id = request.POST.get('group_id') group = get_object_or_404(Group, id=group_id) group.delete() messages.success(request, _("Group deleted.")) elif action == 'toggle_status': user_id = request.POST.get('user_id') user = get_object_or_404(User, id=user_id) if user == request.user: messages.error(request, _("You cannot deactivate yourself.")) else: user.is_active = not user.is_active user.save() messages.success(request, f"User {user.username} status updated.") # Determine redirect hash based on action target_hash = "" if action in ['add_group', 'edit_group', 'delete_group']: target_hash = "#groups" return redirect(reverse('user_management') + target_hash) return render(request, 'core/users.html', { 'users': users, 'groups': groups, 'permissions': permissions }) @login_required def group_details_api(request, pk): group = get_object_or_404(Group, pk=pk) permissions = group.permissions.all().values_list('id', flat=True) return JsonResponse({ 'id': group.id, 'name': group.name, 'permissions': list(permissions) }) @csrf_exempt @login_required def add_payment_method_ajax(request): if request.method == 'POST': try: data = json.loads(request.body) name_en = data.get('name_en') name_ar = data.get('name_ar') is_active = data.get('is_active', True) if not name_en or not name_ar: return JsonResponse({'success': False, 'error': 'Missing names'}, status=400) pm = PaymentMethod.objects.create(name_en=name_en, name_ar=name_ar, is_active=is_active) return JsonResponse({ 'success': True, 'id': pm.id, 'name_en': pm.name_en, 'name_ar': pm.name_ar }) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) @csrf_exempt @login_required def add_customer_ajax(request): if request.method == 'POST': try: data = json.loads(request.body) name = data.get('name') phone = data.get('phone', '') email = data.get('email', '') address = data.get('address', '') if not name: return JsonResponse({'success': False, 'error': 'Missing name'}, status=400) customer = Customer.objects.create(name=name, phone=phone, email=email, address=address) return JsonResponse({ 'success': True, 'id': customer.id, 'name': customer.name }) 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 hold_sale_api(request): if request.method == 'POST': try: data = json.loads(request.body) customer_id = data.get('customer_id') cart_data = data.get('items', []) total_amount = data.get('total_amount', 0) notes = data.get('notes', '') customer = None if customer_id: customer = Customer.objects.filter(id=customer_id).first() held_sale = HeldSale.objects.create( customer=customer, cart_data=cart_data, total_amount=total_amount, notes=notes, created_by=request.user ) return JsonResponse({'success': True, 'held_id': held_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 get_held_sales_api(request): held_sales = HeldSale.objects.filter(created_by=request.user).select_related('customer').order_by('-created_at') data = [] for hs in held_sales: data.append({ 'id': hs.id, 'customer_name': hs.customer.name if hs.customer else 'Guest', 'total_amount': float(hs.total_amount), 'items_count': len(hs.cart_data), 'created_at': hs.created_at.strftime("%Y-%m-%d %H:%M"), 'notes': hs.notes }) return JsonResponse({'success': True, 'held_sales': data}) @login_required def recall_held_sale_api(request, pk): held_sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user) data = { 'success': True, 'customer_id': held_sale.customer.id if held_sale.customer else None, 'customer_name': held_sale.customer.name if held_sale.customer else "", 'items': held_sale.cart_data, 'total_amount': float(held_sale.total_amount), 'notes': held_sale.notes } held_sale.delete() return JsonResponse(data) @login_required def delete_held_sale_api(request, pk): held_sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user) held_sale.delete() return JsonResponse({'success': True}) @login_required def add_loyalty_tier(request): if request.method == 'POST': name_en = request.POST.get('name_en') name_ar = request.POST.get('name_ar') min_points = request.POST.get('min_points', 0) multiplier = request.POST.get('point_multiplier', 1.0) discount = request.POST.get('discount_percentage', 0) color = request.POST.get('color_code', '#6c757d') LoyaltyTier.objects.create( name_en=name_en, name_ar=name_ar, min_points=min_points, point_multiplier=multiplier, discount_percentage=discount, color_code=color ) messages.success(request, _("Loyalty tier added successfully!")) return redirect(reverse('settings') + '#loyalty') @login_required def edit_loyalty_tier(request, pk): tier = get_object_or_404(LoyaltyTier, pk=pk) if request.method == 'POST': tier.name_en = request.POST.get('name_en') tier.name_ar = request.POST.get('name_ar') tier.min_points = request.POST.get('min_points') tier.point_multiplier = request.POST.get('point_multiplier') tier.discount_percentage = request.POST.get('discount_percentage') tier.color_code = request.POST.get('color_code') tier.save() messages.success(request, _("Loyalty tier updated successfully!")) return redirect(reverse('settings') + '#loyalty') @login_required def delete_loyalty_tier(request, pk): tier = get_object_or_404(LoyaltyTier, pk=pk) tier.delete() messages.success(request, _("Loyalty tier deleted successfully!")) return redirect(reverse('settings') + '#loyalty') @login_required def get_customer_loyalty_api(request, pk): customer = get_object_or_404(Customer, pk=pk) settings = SystemSetting.objects.first() tier_info = None if customer.loyalty_tier: tier_info = { 'name_en': customer.loyalty_tier.name_en, 'name_ar': customer.loyalty_tier.name_ar, 'multiplier': float(customer.loyalty_tier.point_multiplier), 'discount': float(customer.loyalty_tier.discount_percentage), 'color': customer.loyalty_tier.color_code } return JsonResponse({ 'success': True, 'points': float(customer.loyalty_points), 'tier': tier_info, 'currency_per_point': float(settings.currency_per_point) if settings else 0.01, 'min_points_to_redeem': settings.min_points_to_redeem if settings else 100 }) @login_required def profile_view(request): """ User Profile View """ if request.method == 'POST': user = request.user user.first_name = request.POST.get('first_name') user.last_name = request.POST.get('last_name') user.email = request.POST.get('email') # Profile specific profile = user.profile profile.phone = request.POST.get('phone') profile.bio = request.POST.get('bio') if 'image' in request.FILES: profile.image = request.FILES['image'] user.save() profile.save() # Password change password = request.POST.get('password') confirm_password = request.POST.get('confirm_password') if password: if password == confirm_password: user.set_password(password) user.save() from django.contrib.auth import update_session_auth_hash update_session_auth_hash(request, user) messages.success(request, _("Profile and password updated successfully!")) else: messages.error(request, _("Passwords do not match.")) else: messages.success(request, _("Profile updated successfully!")) return redirect('profile') return render(request, 'core/profile.html') # --- Expenses Views --- @login_required def expenses_view(request): """ List and filter expenses """ expenses = Expense.objects.all().order_by('-date', '-created_at') # Filtering start_date = request.GET.get('start_date') end_date = request.GET.get('end_date') category_id = request.GET.get('category') if start_date: expenses = expenses.filter(date__gte=start_date) if end_date: expenses = expenses.filter(date__lte=end_date) if category_id: expenses = expenses.filter(category_id=category_id) paginator = Paginator(expenses, 25) page_number = request.GET.get('page') expenses = paginator.get_page(page_number) categories = ExpenseCategory.objects.all() payment_methods = PaymentMethod.objects.filter(is_active=True) context = { 'expenses': expenses, 'categories': categories, 'payment_methods': payment_methods, 'start_date': start_date, 'end_date': end_date, 'category_id': category_id, } return render(request, 'core/expenses.html', context) @login_required def expense_create_view(request): """ Create a new expense """ if request.method == 'POST': category_id = request.POST.get('category') amount = request.POST.get('amount') date = request.POST.get('date') or timezone.now().date() description = request.POST.get('description', '') payment_method_id = request.POST.get('payment_method') attachment = request.FILES.get('attachment') category = get_object_or_404(ExpenseCategory, id=category_id) pm = None if payment_method_id: pm = get_object_or_404(PaymentMethod, id=payment_method_id) Expense.objects.create( category=category, amount=amount, date=date, description=description, payment_method=pm, attachment=attachment, created_by=request.user ) messages.success(request, _("Expense recorded successfully!")) return redirect('expenses') @login_required def expense_delete_view(request, pk): """ Delete an expense """ 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): """ Manage expense categories """ 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: category = get_object_or_404(ExpenseCategory, id=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: ExpenseCategory.objects.create( name_en=name_en, name_ar=name_ar, description=description ) messages.success(request, _("Expense category created successfully!")) return redirect('expense_categories') categories = ExpenseCategory.objects.all().order_by('name_en') return render(request, 'core/expense_categories.html', {'categories': categories}) @login_required def expense_category_delete_view(request, pk): """ Delete an expense category """ category = get_object_or_404(ExpenseCategory, pk=pk) category.delete() messages.success(request, _("Expense category deleted successfully!")) return redirect('expense_categories') @csrf_exempt @login_required def update_sale_api(request, pk): if request.method == 'POST': try: sale = get_object_or_404(Sale, pk=pk) data = json.loads(request.body) customer_id = data.get('customer_id') invoice_number = data.get('invoice_number', '') items = data.get('items', []) total_amount = data.get('total_amount', 0) paid_amount = data.get('paid_amount', 0) discount = data.get('discount', 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', '') points_to_redeem = data.get('loyalty_points_redeemed', 0) settings = SystemSetting.objects.first() if not settings: settings = SystemSetting.objects.create() # 1. Restore Stock for item in sale.items.all(): item.product.stock_quantity += item.quantity item.product.save() # 2. Reverse Loyalty Points for the old customer if sale.customer and settings.loyalty_enabled: for lt in sale.loyalty_transactions.all(): sale.customer.loyalty_points -= decimal.Decimal(str(lt.points)) lt.delete() sale.customer.update_tier() sale.customer.save() # 3. Update Sale Metadata customer = None if customer_id: customer = Customer.objects.get(id=customer_id) if not customer and payment_type != 'cash': return JsonResponse({'success': False, 'error': _('Credit or Partial payments are not allowed for Guest customers.')}, status=400) sale.customer = customer sale.invoice_number = invoice_number sale.total_amount = total_amount sale.discount = discount sale.payment_type = payment_type sale.due_date = due_date if due_date else None sale.notes = notes # Loyalty discount recalculation loyalty_discount = 0 if settings.loyalty_enabled and customer and points_to_redeem > 0: if float(customer.loyalty_points) >= float(points_to_redeem): loyalty_discount = float(points_to_redeem) * float(settings.currency_per_point) sale.loyalty_points_redeemed = points_to_redeem sale.loyalty_discount_amount = loyalty_discount sale.save() # 4. Handle Items (Delete old, Create new) sale.items.all().delete() for item in items: product = Product.objects.get(id=item['id']) SaleItem.objects.create( sale=sale, product=product, quantity=item['quantity'], unit_price=item['price'], line_total=item['line_total'] ) product.stock_quantity -= int(item['quantity']) product.save() # 5. Handle Payments sale.paid_amount = paid_amount sale.balance_due = float(total_amount) - float(paid_amount) if float(paid_amount) >= float(total_amount): sale.status = 'paid' elif float(paid_amount) > 0: sale.status = 'partial' else: sale.status = 'unpaid' sale.save() pm = None if payment_method_id: pm = PaymentMethod.objects.filter(id=payment_method_id).first() initial_payment = sale.payments.filter(notes="Initial payment").first() if initial_payment: initial_payment.amount = paid_amount initial_payment.payment_method = pm initial_payment.payment_method_name = pm.name_en if pm else payment_type.capitalize() initial_payment.save() elif float(paid_amount) > 0: SalePayment.objects.create( sale=sale, amount=paid_amount, payment_method=pm, payment_method_name=pm.name_en if pm else payment_type.capitalize(), notes="Initial payment", created_by=request.user ) # 6. Re-apply Loyalty for the (possibly new) customer if settings.loyalty_enabled and customer: points_earned = float(total_amount) * float(settings.points_per_currency) if customer.loyalty_tier: points_earned *= float(customer.loyalty_tier.point_multiplier) if points_earned > 0: customer.loyalty_points += decimal.Decimal(str(points_earned)) LoyaltyTransaction.objects.create( customer=customer, sale=sale, transaction_type='earned', points=points_earned, notes=f"Points earned from Updated Sale #{sale.id}" ) if points_to_redeem > 0: customer.loyalty_points -= decimal.Decimal(str(points_to_redeem)) LoyaltyTransaction.objects.create( customer=customer, sale=sale, transaction_type='redeemed', points=-points_to_redeem, notes=f"Points redeemed for Updated Sale #{sale.id}" ) customer.update_tier() customer.save() return JsonResponse({'success': True}) except Exception as e: import traceback traceback.print_exc() return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) @login_required def customer_payments(request): """ List of payments received from customers """ payments_qs = SalePayment.objects.all().select_related("sale", "sale__customer", "payment_method", "created_by").order_by("-payment_date", "-id") # Filtering start_date = request.GET.get("start_date") end_date = request.GET.get("end_date") customer_id = request.GET.get("customer") if start_date: payments_qs = payments_qs.filter(payment_date__gte=start_date) if end_date: payments_qs = payments_qs.filter(payment_date__lte=end_date) if customer_id: payments_qs = payments_qs.filter(sale__customer_id=customer_id) paginator = Paginator(payments_qs, 25) page_number = request.GET.get("page") payments = paginator.get_page(page_number) customers = Customer.objects.all().order_by("name") return render(request, "core/customer_payments.html", { "payments": payments, "customers": customers, "start_date": start_date, "end_date": end_date, "customer_id": customer_id, }) @login_required @login_required def sale_receipt(request, pk): """ Printable receipt for a fully paid sale """ sale = get_object_or_404(Sale, pk=pk) settings = SystemSetting.objects.first() return render(request, "core/sale_receipt.html", { "sale": sale, "settings": settings, "amount_in_words": number_to_words_en(sale.total_amount) }) def customer_payment_receipt(request, pk): """ Printable receipt for a customer payment """ payment = get_object_or_404(SalePayment, pk=pk) settings = SystemSetting.objects.first() return render(request, "core/customer_payment_receipt.html", { "payment": payment, "settings": settings, "amount_in_words": number_to_words_en(payment.amount) }) @login_required def customer_statement(request): """ Generate a transaction statement for a specific customer. """ customers = Customer.objects.all().order_by('name') customer_id = request.GET.get('customer') start_date = request.GET.get('start_date') end_date = request.GET.get('end_date') statement_data = [] customer = None opening_balance = 0 if customer_id: customer = get_object_or_404(Customer, id=customer_id) # Calculate opening balance before start_date if start_date: sales_before = Sale.objects.filter(customer=customer, created_at__date__lt=start_date).aggregate(total=Sum('total_amount'))['total'] or 0 returns_before = SaleReturn.objects.filter(customer=customer, created_at__date__lt=start_date).aggregate(total=Sum('total_amount'))['total'] or 0 payments_before = SalePayment.objects.filter(sale__customer=customer, payment_date__lt=start_date).aggregate(total=Sum('amount'))['total'] or 0 opening_balance = float(sales_before) - float(returns_before) - float(payments_before) # Fetch transactions within range sales = Sale.objects.filter(customer=customer) returns = SaleReturn.objects.filter(customer=customer) payments = SalePayment.objects.filter(sale__customer=customer) if start_date: sales = sales.filter(created_at__date__gte=start_date) returns = returns.filter(created_at__date__gte=start_date) payments = payments.filter(payment_date__gte=start_date) if end_date: sales = sales.filter(created_at__date__lte=end_date) returns = returns.filter(created_at__date__lte=end_date) payments = payments.filter(payment_date__lte=end_date) for sale in sales: statement_data.append({ 'date': sale.created_at.date(), 'type': _('Sale Invoice'), 'reference': sale.invoice_number or f"#{sale.id}", 'debit': float(sale.total_amount), 'credit': 0, }) for ret in returns: statement_data.append({ 'date': ret.created_at.date(), 'type': _('Sale Return'), 'reference': ret.return_number or f"#{ret.id}", 'debit': 0, 'credit': float(ret.total_amount), }) for pay in payments: statement_data.append({ 'date': pay.payment_date, 'type': _('Payment'), 'reference': pay.notes or _('Payment Received'), 'debit': 0, 'credit': float(pay.amount), }) statement_data.sort(key=lambda x: (x['date'], x['type'])) running_balance = opening_balance for item in statement_data: running_balance += item['debit'] - item['credit'] item['balance'] = running_balance settings = SystemSetting.objects.first() context = { 'customers': customers, 'customer': customer, 'statement_data': statement_data, 'opening_balance': opening_balance, 'opening_balance_abs': abs(opening_balance), 'start_date': start_date, 'end_date': end_date, 'settings': settings } return render(request, 'core/customer_statement.html', context) @login_required def supplier_statement(request): """ Generate a transaction statement for a specific supplier. """ suppliers = Supplier.objects.all().order_by('name') supplier_id = request.GET.get('supplier') start_date = request.GET.get('start_date') end_date = request.GET.get('end_date') statement_data = [] supplier = None opening_balance = 0 if supplier_id: supplier = get_object_or_404(Supplier, id=supplier_id) # Calculate opening balance before start_date if start_date: purchases_before = Purchase.objects.filter(supplier=supplier, created_at__date__lt=start_date).aggregate(total=Sum('total_amount'))['total'] or 0 returns_before = PurchaseReturn.objects.filter(supplier=supplier, created_at__date__lt=start_date).aggregate(total=Sum('total_amount'))['total'] or 0 payments_before = PurchasePayment.objects.filter(purchase__supplier=supplier, payment_date__lt=start_date).aggregate(total=Sum('amount'))['total'] or 0 opening_balance = float(purchases_before) - float(returns_before) - float(payments_before) # Fetch transactions within range purchases = Purchase.objects.filter(supplier=supplier) returns = PurchaseReturn.objects.filter(supplier=supplier) payments = PurchasePayment.objects.filter(purchase__supplier=supplier) if start_date: purchases = purchases.filter(created_at__date__gte=start_date) returns = returns.filter(created_at__date__gte=start_date) payments = payments.filter(payment_date__gte=start_date) if end_date: purchases = purchases.filter(created_at__date__lte=end_date) returns = returns.filter(created_at__date__lte=end_date) payments = payments.filter(payment_date__lte=end_date) for purchase in purchases: statement_data.append({ 'date': purchase.created_at.date(), 'type': _('Purchase Invoice'), 'reference': purchase.invoice_number or f"#{purchase.id}", 'debit': float(purchase.total_amount), 'credit': 0, }) for ret in returns: statement_data.append({ 'date': ret.created_at.date(), 'type': _('Purchase Return'), 'reference': ret.return_number or f"#{ret.id}", 'debit': 0, 'credit': float(ret.total_amount), }) for pay in payments: statement_data.append({ 'date': pay.payment_date, 'type': _('Payment'), 'reference': pay.notes or _('Payment Sent'), 'debit': 0, 'credit': float(pay.amount), }) statement_data.sort(key=lambda x: (x['date'], x['type'])) running_balance = opening_balance for item in statement_data: running_balance += item['debit'] - item['credit'] item['balance'] = running_balance settings = SystemSetting.objects.first() context = { 'suppliers': suppliers, 'supplier': supplier, 'statement_data': statement_data, 'opening_balance': opening_balance, 'opening_balance_abs': abs(opening_balance), 'start_date': start_date, 'end_date': end_date, 'settings': settings } return render(request, 'core/supplier_statement.html', context) @login_required def cashflow_report(request): """ Generate a Cashflow report summarizing income and expenses. """ start_date = request.GET.get('start_date') end_date = request.GET.get('end_date') # Defaults to current month if no dates provided if not start_date: start_date = timezone.now().date().replace(day=1).strftime('%Y-%m-%d') if not end_date: end_date = timezone.now().date().strftime('%Y-%m-%d') # Fetching Inflows (Sale Payments) sale_payments = SalePayment.objects.all().select_related('sale', 'sale__customer') if start_date: sale_payments = sale_payments.filter(payment_date__gte=start_date) if end_date: sale_payments = sale_payments.filter(payment_date__lte=end_date) # Fetching Outflows (Purchase Payments) purchase_payments = PurchasePayment.objects.all().select_related('purchase', 'purchase__supplier') if start_date: purchase_payments = purchase_payments.filter(payment_date__gte=start_date) if end_date: purchase_payments = purchase_payments.filter(payment_date__lte=end_date) # Fetching Outflows (Expenses) expenses = Expense.objects.all().select_related('category', 'payment_method') if start_date: expenses = expenses.filter(date__gte=start_date) if end_date: expenses = expenses.filter(date__lte=end_date) # Prepare detailed transactions list transactions = [] for pay in sale_payments: transactions.append({ 'date': pay.payment_date, 'type': _('Sale Payment'), 'reference': pay.sale.invoice_number or f"Sale #{pay.sale.id}", 'contact': pay.sale.customer.name if pay.sale.customer else _('Guest'), 'inflow': float(pay.amount), 'outflow': 0, 'method': pay.payment_method_name }) for pay in purchase_payments: transactions.append({ 'date': pay.payment_date, 'type': _('Purchase Payment'), 'reference': pay.purchase.invoice_number or f"Purchase #{pay.purchase.id}", 'contact': pay.purchase.supplier.name if pay.purchase.supplier else 'N/A', 'inflow': 0, 'outflow': float(pay.amount), 'method': pay.payment_method_name }) for exp in expenses: transactions.append({ 'date': exp.date, 'type': _('Expense'), 'reference': exp.category.name_en, 'contact': _('Various'), 'inflow': 0, 'outflow': float(exp.amount), 'method': exp.payment_method.name_en if exp.payment_method else _('N/A') }) transactions.sort(key=lambda x: x['date'], reverse=True) total_inflow = sum(item['inflow'] for item in transactions) total_outflow = sum(item['outflow'] for item in transactions) net_cashflow = total_inflow - total_outflow settings = SystemSetting.objects.first() context = { 'transactions': transactions, 'total_inflow': total_inflow, 'total_outflow': total_outflow, 'net_cashflow': net_cashflow, 'start_date': start_date, 'end_date': end_date, 'settings': settings } return render(request, 'core/cashflow_report.html', context) @login_required def test_whatsapp_connection(request): """ AJAX view to test the WhatsApp connection. """ if request.method == 'POST': try: data = json.loads(request.body) phone = data.get('phone') if not phone: return JsonResponse({'success': False, 'error': _("Phone number is required.")}) from .utils import send_whatsapp_message success, message = send_whatsapp_message(phone, _("Hello! This is a test message from your Meezan Smart Admin WhatsApp Gateway.")) return JsonResponse({'success': success, 'message': message}) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}) return JsonResponse({'success': False, 'error': _("Invalid request method.")}) @login_required def add_device(request): if request.method == 'POST': 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' Device.objects.create( name=name, device_type=device_type, connection_type=connection_type, ip_address=ip_address if ip_address else None, port=port if port else None, is_active=is_active ) 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' if not device.ip_address: device.ip_address = None if not device.port: device.port = None device.save() messages.success(request, _("Device updated successfully!")) return redirect(reverse('settings') + '#devices') @login_required def delete_device(request, pk): device = get_object_or_404(Device, pk=pk) device.delete() messages.success(request, _("Device deleted successfully!")) return redirect(reverse('settings') + '#devices') @login_required def search_customers_api(request): query = request.GET.get('q', '') if query: customers = Customer.objects.filter( Q(name__icontains=query) | Q(phone__icontains=query) ).values('id', 'name', 'phone')[:20] else: customers = [] return JsonResponse({'results': list(customers)}) @login_required def customer_display(request): """ Render the Customer Facing Display screen. """ settings = SystemSetting.objects.first() return render(request, "core/customer_display.html", {"settings": settings}) @csrf_exempt def pos_sync_update(request): """ Saves the POS cart state to the user's session for the Customer Display to pick up. """ if request.method == 'POST': try: data = json.loads(request.body) # Store the cart data in the session request.session['pos_cart_state'] = data request.session.modified = True return JsonResponse({'status': 'ok', 'timestamp': timezone.now().timestamp()}) except Exception as e: return JsonResponse({'status': 'error', 'message': str(e)}, status=400) return JsonResponse({'status': 'error', 'message': 'Invalid method'}, status=405) def pos_sync_state(request): """ Returns the POS cart state from the session. """ data = request.session.get('pos_cart_state', None) if data is None: # Return a special flag if no state is found yet return JsonResponse({'status': 'empty'}, safe=False) return JsonResponse(data, safe=False) # --- LPO Views --- @login_required def lpo_list(request): orders = PurchaseOrder.objects.all().select_related('supplier', 'created_by').order_by('-created_at') paginator = Paginator(orders, 25) page_number = request.GET.get('page') orders_list = paginator.get_page(page_number) return render(request, 'core/lpo_list.html', {'orders': orders_list}) @login_required def lpo_create(request): products = Product.objects.filter(is_active=True) suppliers = Supplier.objects.all() return render(request, 'core/lpo_create.html', { 'products': products, 'suppliers': suppliers, }) @login_required def lpo_detail(request, pk): order = get_object_or_404(PurchaseOrder, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/lpo_detail.html', { 'order': order, 'settings': settings, 'amount_in_words': number_to_words_en(order.total_amount) }) @csrf_exempt @login_required def create_lpo_api(request): if request.method == 'POST': try: data = json.loads(request.body) supplier_id = data.get('supplier_id') lpo_number = data.get('lpo_number', '') items = data.get('items', []) total_amount = data.get('total_amount', 0) notes = data.get('notes', '') issue_date = data.get('issue_date') expected_date = data.get('expected_date') supplier = None if supplier_id: supplier = Supplier.objects.get(id=supplier_id) order = PurchaseOrder.objects.create( supplier=supplier, lpo_number=lpo_number, total_amount=total_amount, notes=notes, issue_date=issue_date if issue_date else timezone.now(), expected_date=expected_date if expected_date else None, created_by=request.user, status='draft' ) for item in items: product = Product.objects.get(id=item['id']) PurchaseOrderItem.objects.create( purchase_order=order, product=product, quantity=item['quantity'], cost_price=item['price'], line_total=item['line_total'] ) return JsonResponse({'success': True, 'order_id': order.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 convert_lpo_to_purchase(request, pk): order = get_object_or_404(PurchaseOrder, pk=pk) if order.status == 'converted': messages.warning(request, _("This LPO is already converted.")) return redirect('purchase_detail', pk=order.converted_purchase.first().id) if request.method == 'POST': # Create Purchase purchase = Purchase.objects.create( supplier=order.supplier, purchase_order=order, total_amount=order.total_amount, paid_amount=0, balance_due=order.total_amount, status='unpaid', notes=f"Converted from LPO #{order.id}. {order.notes}", created_by=request.user ) # Copy items and update stock for item in order.items.all(): PurchaseItem.objects.create( purchase=purchase, product=item.product, quantity=item.quantity, cost_price=item.cost_price, line_total=item.line_total ) # Update Stock item.product.stock_quantity += item.quantity item.product.cost_price = item.cost_price # Update cost price item.product.save() order.status = 'converted' order.save() messages.success(request, _("LPO successfully converted to Purchase Invoice.")) return redirect('purchase_detail', pk=purchase.id) # Check if this is a GET request for confirmation page, though we usually just post # But user might want to review. return redirect('lpo_detail', pk=order.id) @login_required def lpo_delete(request, pk): order = get_object_or_404(PurchaseOrder, pk=pk) if order.status == 'converted': messages.error(request, _("Cannot delete converted LPO.")) return redirect('lpo_detail', pk=pk) order.delete() messages.success(request, _("LPO deleted.")) return redirect('lpo_list') @login_required def cashier_registry(request): if not (request.user.is_superuser or request.user.groups.filter(name='admin').exists()): messages.error(request, _("Access denied.")) return redirect('index') if request.method == 'POST': action = request.POST.get('action') if action == 'assign': cashier_id = request.POST.get('cashier_id') counter_id = request.POST.get('counter_id') if cashier_id and counter_id: cashier = get_object_or_404(User, id=cashier_id) counter = get_object_or_404(Device, id=counter_id) # Check if cashier already assigned CashierCounterRegistry.objects.update_or_create( cashier=cashier, defaults={'counter': counter} ) messages.success(request, _("Cashier assigned to counter successfully.")) elif action == 'delete': registry_id = request.POST.get('registry_id') reg = get_object_or_404(CashierCounterRegistry, id=registry_id) reg.delete() messages.success(request, _("Assignment removed.")) return redirect('cashier_registry') registries = CashierCounterRegistry.objects.select_related('cashier', 'counter').all() # Cashiers not currently assigned (optional logic, but here we list all) counters = Device.objects.filter(device_type='counter', is_active=True) all_cashiers = User.objects.filter(is_active=True).order_by('username') return render(request, 'core/cashier_registry.html', { 'registries': registries, 'counters': counters, 'cashiers': all_cashiers }) # --- Cashier Session Views --- from .forms import CashierSessionStartForm, CashierSessionCloseForm @login_required def cashier_session_list(request): from .models import CashierSession sessions = CashierSession.objects.all().order_by('-start_time') return render(request, 'core/cashier_sessions.html', {'sessions': sessions}) @login_required def start_session(request): from .models import CashierSession # Check if user already has an active session active_session = CashierSession.objects.filter(user=request.user, status='active').first() if active_session: messages.warning(request, _("You already have an active session.")) return redirect('pos') # Check if user is assigned to a counter try: registry = request.user.counter_assignment except: messages.error(request, _("You are not assigned to any counter.")) return redirect('index') if request.method == 'POST': form = CashierSessionStartForm(request.POST) if form.is_valid(): session = form.save(commit=False) session.user = request.user session.counter = registry.counter session.save() messages.success(request, _("Session started successfully.")) return redirect('pos') else: form = CashierSessionStartForm() return render(request, 'core/session_start.html', {'form': form, 'counter': registry.counter}) @login_required def close_session(request): from .models import CashierSession active_session = CashierSession.objects.filter(user=request.user, status='active').first() if not active_session: messages.error(request, _("No active session found.")) return redirect('index') if request.method == 'POST': form = CashierSessionCloseForm(request.POST, instance=active_session) if form.is_valid(): session = form.save(commit=False) session.status = 'closed' session.end_time = timezone.now() session.save() messages.success(request, _("Session closed successfully.")) return redirect('session_detail', pk=session.pk) else: form = CashierSessionCloseForm(instance=active_session) # Calculate totals for information sales = Sale.objects.filter(created_by=request.user, created_at__gte=active_session.start_time) total_sales = sales.aggregate(Sum('total_amount'))['total_amount__sum'] or 0 # Calculate payments by method payments = SalePayment.objects.filter(created_by=request.user, created_at__gte=active_session.start_time).values('payment_method_name').annotate(total=Sum('amount')) return render(request, 'core/session_close.html', { 'form': form, 'session': active_session, 'total_sales': total_sales, 'payments': payments }) @login_required def session_detail(request, pk): from .models import CashierSession session = get_object_or_404(CashierSession, pk=pk) # Calculate totals end_time = session.end_time or timezone.now() sales = Sale.objects.filter(created_by=session.user, created_at__gte=session.start_time, created_at__lte=end_time) total_sales = sales.aggregate(Sum('total_amount'))['total_amount__sum'] or 0 payments = SalePayment.objects.filter(created_by=session.user, created_at__gte=session.start_time, created_at__lte=end_time).values('payment_method_name').annotate(total=Sum('amount')) return render(request, 'core/session_detail.html', { 'session': session, 'total_sales': total_sales, 'payments': payments, 'sales_count': sales.count() })