diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 6874f2f..b57f53e 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/views.py b/core/views.py index 0f73854..38dff8c 100644 --- a/core/views.py +++ b/core/views.py @@ -1,5 +1,6 @@ import json import decimal +import logging from django.shortcuts import render, redirect, get_object_or_404 from django.http import JsonResponse, HttpResponse from django.contrib.auth.decorators import login_required @@ -8,36 +9,128 @@ from django.utils import timezone from django.contrib import messages from django.db import transaction from django.db.models import Sum, Q, Count, F +from django.db.models.functions import TruncMonth, TruncDay from django.core.paginator import Paginator from django.urls import reverse +from django.utils.text import slugify from .models import ( Product, Category, Unit, Supplier, Customer, Sale, SaleItem, Purchase, PurchaseItem, Expense, ExpenseCategory, SalePayment, PurchasePayment, SystemSetting, CashierSession, SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem, HeldSale, Quotation, QuotationItem, PaymentMethod, LoyaltyTier, - Device, CashierCounterRegistry, UserProfile + Device, CashierCounterRegistry, UserProfile, PurchaseOrder, PurchaseOrderItem ) -from .forms import SystemSettingForm, CustomerForm, SupplierForm, ProductForm, CategoryForm, UnitForm +from .forms import SystemSettingForm, CustomerForm, SupplierForm, ProductForm, CategoryForm, UnitForm, ExpenseForm +from .helpers import number_to_words_en + +logger = logging.getLogger(__name__) # --- Dashboard --- @login_required def index(request): + settings = SystemSetting.objects.first() today = timezone.now().date() - # Stats - today_sales = Sale.objects.filter(created_at__date=today).aggregate(t=Sum('total_amount'))['t'] or 0 - month_sales = Sale.objects.filter(created_at__month=today.month).aggregate(t=Sum('total_amount'))['t'] or 0 - low_stock = Product.objects.filter(stock_quantity__lte=F('min_stock_level'), is_active=True).count() + seven_days_ago = today - timezone.timedelta(days=7) + this_year = today.year + + # 1. Financial Headlines + total_sales_amount = Sale.objects.aggregate(t=Sum('total_amount'))['t'] or 0 + total_receivables = Sale.objects.filter(status__in=['partial', 'unpaid']).aggregate(t=Sum('balance_due'))['t'] or 0 + # For payables, we sum the balance_due of Purchases + total_payables = Purchase.objects.filter(status__in=['partial', 'unpaid']).aggregate(t=Sum('balance_due'))['t'] or 0 + + # 2. Counts + total_sales_count = Sale.objects.count() total_products = Product.objects.filter(is_active=True).count() + total_customers = Customer.objects.count() + + # 3. Monthly Sales Chart (This Year) + monthly_sales = Sale.objects.filter(created_at__year=this_year)\ + .annotate(month=TruncMonth('created_at'))\ + .values('month')\ + .annotate(total=Sum('total_amount'))\ + .order_by('month') - # Recent Sales - recent_sales = Sale.objects.select_related('customer').order_by('-created_at')[:5] + monthly_labels = [] + monthly_data = [] + # Initialize all months to 0 + months_map = {i: 0 for i in range(1, 13)} + for entry in monthly_sales: + months_map[entry['month'].month] = float(entry['total']) + import calendar + for i in range(1, 13): + monthly_labels.append(calendar.month_name[i]) + monthly_data.append(months_map[i]) + + # 4. Daily Sales Chart (Last 7 Days) + daily_sales = Sale.objects.filter(created_at__date__gte=seven_days_ago)\ + .annotate(day=TruncDay('created_at'))\ + .values('day')\ + .annotate(total=Sum('total_amount'))\ + .order_by('day') + + daily_map = {} + for entry in daily_sales: + daily_map[entry['day'].date()] = float(entry['total']) + + chart_labels = [] + chart_data = [] + for i in range(7): + day = seven_days_ago + timezone.timedelta(days=i) + chart_labels.append(day.strftime('%a %d')) + chart_data.append(daily_map.get(day, 0)) + + # 5. Sales by Category + category_sales = SaleItem.objects.values('product__category__name_en')\ + .annotate(total=Sum('line_total'))\ + .order_by('-total')[:6] + + category_labels = [item['product__category__name_en'] for item in category_sales] + category_data = [float(item['total']) for item in category_sales] + + # 6. Payment Methods + payment_stats = SalePayment.objects.values('payment_method__name_en')\ + .annotate(total=Sum('amount'))\ + .order_by('-total') + + payment_labels = [item['payment_method__name_en'] or 'Cash' for item in payment_stats] + payment_data = [float(item['total']) for item in payment_stats] + + # 7. Top Selling Products + top_products = SaleItem.objects.values('product__name_en', 'product__name_ar')\ + .annotate(total_qty=Sum('quantity'), total_rev=Sum('line_total'))\ + .order_by('-total_qty')[:5] + + # 8. Low Stock & Expiry + low_stock_products = Product.objects.filter(stock_quantity__lte=F('min_stock_level'), is_active=True)[:5] + low_stock_count = Product.objects.filter(stock_quantity__lte=F('min_stock_level'), is_active=True).count() + expired_count = Product.objects.filter(has_expiry=True, expiry_date__lt=today).count() + + # 9. Recent Sales + recent_sales = Sale.objects.select_related('customer', 'created_by').order_by('-created_at')[:5] + context = { - 'today_sales': today_sales, - 'month_sales': month_sales, - 'low_stock_count': low_stock, + 'site_settings': settings, + 'total_sales_amount': total_sales_amount, + 'total_receivables': total_receivables, + 'total_payables': total_payables, + 'total_sales_count': total_sales_count, 'total_products': total_products, + 'total_customers': total_customers, + 'monthly_labels': monthly_labels, + 'monthly_data': monthly_data, + 'chart_labels': chart_labels, + 'chart_data': chart_data, + 'category_labels': category_labels, + 'category_data': category_data, + 'payment_labels': payment_labels, + 'payment_data': payment_data, + 'top_products': top_products, + 'low_stock_products': low_stock_products, + 'low_stock_count': low_stock_count, + 'expired_count': expired_count, 'recent_sales': recent_sales, } return render(request, 'core/index.html', context) @@ -84,14 +177,21 @@ def add_category_ajax(request): if request.method != 'POST': return JsonResponse({'success': False, 'error': 'Invalid method'}) try: - data = json.loads(request.body) - name_en = data.get('name_en') - name_ar = data.get('name_ar') + try: + data = json.loads(request.body) + name_en = data.get('name_en') + name_ar = data.get('name_ar') + except (json.JSONDecodeError, TypeError): + name_en = request.POST.get('name_en') + name_ar = request.POST.get('name_ar') + if not name_en or not name_ar: return JsonResponse({'success': False, 'error': 'Names are required'}) - # Slug generation (simple) - base_slug = name_en.lower().replace(' ', '-') + base_slug = slugify(name_en) + if not base_slug: + base_slug = f"cat-{timezone.now().strftime('%Y%m%d%H%M%S')}" + slug = base_slug counter = 1 while Category.objects.filter(slug=slug).exists(): @@ -101,6 +201,7 @@ def add_category_ajax(request): Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug) return JsonResponse({'success': True}) except Exception as e: + logger.error(f"Error adding category: {e}") return JsonResponse({'success': False, 'error': str(e)}) @csrf_exempt @@ -109,10 +210,15 @@ def add_unit_ajax(request): if request.method != 'POST': return JsonResponse({'success': False, 'error': 'Invalid method'}) try: - data = json.loads(request.body) - name_en = data.get('name_en') - name_ar = data.get('name_ar') - short_name = data.get('short_name') + try: + data = json.loads(request.body) + name_en = data.get('name_en') + name_ar = data.get('name_ar') + short_name = data.get('short_name') + except (json.JSONDecodeError, TypeError): + name_en = request.POST.get('name_en') + name_ar = request.POST.get('name_ar') + short_name = request.POST.get('short_name') if not name_en or not name_ar or not short_name: return JsonResponse({'success': False, 'error': 'All fields are required'}) @@ -120,6 +226,7 @@ def add_unit_ajax(request): Unit.objects.create(name_en=name_en, name_ar=name_ar, short_name=short_name) return JsonResponse({'success': True}) except Exception as e: + logger.error(f"Error adding unit: {e}") return JsonResponse({'success': False, 'error': str(e)}) @login_required @@ -129,8 +236,17 @@ def add_category(request): name_en = request.POST.get('name_en') name_ar = request.POST.get('name_ar') if name_en and name_ar: - Category.objects.create(name_en=name_en, name_ar=name_ar, slug=name_en.lower().replace(' ', '-')) - messages.success(request, "Category added!") + try: + base_slug = slugify(name_en) or f"cat-{timezone.now().timestamp()}" + slug = base_slug + counter = 1 + while Category.objects.filter(slug=slug).exists(): + slug = f"{base_slug}-{counter}" + counter += 1 + Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug) + messages.success(request, "Category added!") + except Exception as e: + messages.error(request, f"Error: {e}") return redirect('inventory') @login_required @@ -152,12 +268,15 @@ def delete_category(request, pk): @login_required def add_unit(request): if request.method == 'POST': - Unit.objects.create( - name_en=request.POST.get('name_en'), - name_ar=request.POST.get('name_ar'), - short_name=request.POST.get('short_name') - ) - messages.success(request, "Unit added!") + try: + Unit.objects.create( + name_en=request.POST.get('name_en'), + name_ar=request.POST.get('name_ar'), + short_name=request.POST.get('short_name') + ) + messages.success(request, "Unit added!") + except Exception as e: + messages.error(request, f"Error: {e}") return redirect('inventory') @login_required @@ -316,12 +435,10 @@ def create_sale_api(request): ) sale.subtotal = subtotal - # VAT calc (simplified) sale.total_amount = subtotal - decimal.Decimal(str(sale.discount)) - sale.paid_amount = sale.total_amount # Full payment assumed for POS + sale.paid_amount = sale.total_amount # POS full payment sale.save() - # Record Payment SalePayment.objects.create( sale=sale, amount=sale.paid_amount, @@ -342,7 +459,8 @@ def invoice_list(request): return render(request, 'core/invoices.html', { 'sales': paginator.get_page(request.GET.get('page')), 'customers': Customer.objects.all(), - 'site_settings': SystemSetting.objects.first() + 'site_settings': SystemSetting.objects.first(), + 'payment_methods': PaymentMethod.objects.filter(is_active=True) }) @login_required @@ -355,7 +473,6 @@ def settings_view(request): form = SystemSettingForm(request.POST, request.FILES, instance=settings) if form.is_valid(): s = form.save(commit=False) - # Fix nulls if not s.wablas_server_url: s.wablas_server_url = '' if not s.wablas_secret_key: s.wablas_secret_key = '' if not s.wablas_token: s.wablas_token = '' @@ -378,139 +495,352 @@ def suggest_sku(request): return JsonResponse({'sku': 'SKU-' + timezone.now().strftime("%Y%m%d%H%M%S")}) @login_required -def customer_statement(request): return render(request, 'core/customer_statement.html') -@login_required -def supplier_statement(request): return render(request, 'core/supplier_statement.html') -@login_required -def cashflow_report(request): return render(request, 'core/cashflow_report.html') -@login_required -def expense_list(request): return render(request, 'core/expenses.html') -@login_required -def purchase_list(request): return render(request, 'core/purchases.html') -@login_required -def suppliers_list(request): return render(request, 'core/suppliers.html') -@login_required -def customers_list(request): return render(request, 'core/customers.html') - -# Device Stubs -@login_required -def add_device(request): return redirect('settings') -@login_required -def edit_device(request, pk): return redirect('settings') -@login_required -def delete_device(request, pk): return redirect('settings') - -# POS Sync Stubs -@csrf_exempt -def pos_sync_update(request): return JsonResponse({'status': 'ok'}) -@csrf_exempt -def pos_sync_state(request): return JsonResponse({'state': {}}) - -# Helper for other views -@login_required -def customer_display(request): return render(request, 'core/customer_display.html') -@login_required -def barcode_labels(request): return render(request, 'core/barcodes.html') +def barcode_labels(request): + products = Product.objects.filter(is_active=True).order_by('name_en') + return render(request, 'core/barcode_labels.html', {'products': products}) # --- Customers --- @login_required -def customers(request): return render(request, 'core/customers.html') -@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') +def customers(request): + customers = Customer.objects.all().order_by('name') + paginator = Paginator(customers, 25) + return render(request, 'core/customers.html', { + 'customers': paginator.get_page(request.GET.get('page')) + }) + @csrf_exempt @login_required -def add_customer_ajax(request): return JsonResponse({'success': True}) +def add_customer_ajax(request): + if request.method != 'POST': + return JsonResponse({'success': False, 'error': 'Invalid method'}) + try: + try: + data = json.loads(request.body) + name = data.get('name') + phone = data.get('phone') + except: + name = request.POST.get('name') + phone = request.POST.get('phone') + + if not name: + return JsonResponse({'success': False, 'error': 'Name is required'}) + + Customer.objects.create(name=name, phone=phone or '') + return JsonResponse({'success': True}) + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}) + +@login_required +def add_customer(request): + if request.method == 'POST': + form = CustomerForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, "Customer added") + else: + messages.error(request, "Error adding customer") + return redirect('customers') + +@login_required +def edit_customer(request, pk): + c = get_object_or_404(Customer, pk=pk) + if request.method == 'POST': + c.name = request.POST.get('name') + c.phone = request.POST.get('phone') + c.email = request.POST.get('email') + c.address = request.POST.get('address') + c.save() + messages.success(request, "Customer updated") + return redirect('customers') + +@login_required +def delete_customer(request, pk): + get_object_or_404(Customer, pk=pk).delete() + messages.success(request, "Customer deleted") + return redirect('customers') # --- Suppliers --- @login_required -def suppliers(request): return render(request, 'core/suppliers.html') -@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') +def suppliers(request): + suppliers = Supplier.objects.all().order_by('name') + paginator = Paginator(suppliers, 25) + return render(request, 'core/suppliers.html', { + 'suppliers': paginator.get_page(request.GET.get('page')), + 'site_settings': SystemSetting.objects.first() + }) + @csrf_exempt @login_required -def add_supplier_ajax(request): return JsonResponse({'success': True}) +def add_supplier_ajax(request): + if request.method != 'POST': + return JsonResponse({'success': False, 'error': 'Invalid method'}) + try: + try: + data = json.loads(request.body) + name = data.get('name') + contact_person = data.get('contact_person') + phone = data.get('phone') + except: + name = request.POST.get('name') + contact_person = request.POST.get('contact_person') + phone = request.POST.get('phone') + + if not name: + return JsonResponse({'success': False, 'error': 'Name is required'}) + + Supplier.objects.create(name=name, contact_person=contact_person or '', phone=phone or '') + return JsonResponse({'success': True}) + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}) + +@login_required +def add_supplier(request): + if request.method == 'POST': + form = SupplierForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, "Supplier added") + return redirect('suppliers') + +@login_required +def edit_supplier(request, pk): + s = get_object_or_404(Supplier, pk=pk) + if request.method == 'POST': + s.name = request.POST.get('name') + s.contact_person = request.POST.get('contact_person') + s.phone = request.POST.get('phone') + s.save() + messages.success(request, "Supplier updated") + return redirect('suppliers') + +@login_required +def delete_supplier(request, pk): + get_object_or_404(Supplier, pk=pk).delete() + messages.success(request, "Supplier deleted") + return redirect('suppliers') # --- Purchases --- @login_required -def purchases(request): return render(request, 'core/purchases.html') +def purchases(request): + purchases = Purchase.objects.all().order_by('-created_at') + paginator = Paginator(purchases, 25) + return render(request, 'core/purchases.html', { + 'purchases': paginator.get_page(request.GET.get('page')), + 'payment_methods': PaymentMethod.objects.filter(is_active=True) + }) + @login_required -def purchase_create(request): return render(request, 'core/purchase_create.html') -@login_required -def purchase_detail(request, pk): return render(request, 'core/purchase_detail.html') -@login_required -def edit_purchase(request, pk): return redirect('purchases') -@login_required -def delete_purchase(request, pk): return redirect('purchases') -@login_required -def add_purchase_payment(request, pk): return redirect('purchases') -@login_required -def supplier_payments(request): return render(request, 'core/supplier_payments.html') +def purchase_create(request): + return render(request, 'core/purchase_create.html', { + 'suppliers': Supplier.objects.all(), + 'products': Product.objects.all(), + 'payment_methods': PaymentMethod.objects.filter(is_active=True), + 'site_settings': SystemSetting.objects.first() + }) + @csrf_exempt -def create_purchase_api(request): return JsonResponse({'success': True}) -@csrf_exempt -def update_purchase_api(request, pk): return JsonResponse({'success': True}) +@login_required +def create_purchase_api(request): + if request.method != 'POST': return JsonResponse({'success': False, 'error': 'Invalid method'}) + try: + data = json.loads(request.body) + items = data.get('items', []) + if not items: return JsonResponse({'success': False, 'error': 'No items'}) + + with transaction.atomic(): + purchase = Purchase.objects.create( + supplier_id=data.get('supplier_id'), + invoice_number=data.get('invoice_number', ''), + total_amount=0, + created_by=request.user, + notes=data.get('notes', ''), + status='paid' if data.get('payment_amount') else 'unpaid' # simplified + ) + + total = 0 + for item in items: + qty = decimal.Decimal(str(item['quantity'])) + cost = decimal.Decimal(str(item.get('price', 0))) + line = qty * cost + total += line + + # Update product cost and stock + prod = Product.objects.get(pk=item['id']) + prod.cost_price = cost # Last purchase price logic + prod.stock_quantity += qty + prod.save() + + PurchaseItem.objects.create( + purchase=purchase, + product=prod, + quantity=qty, + cost_price=cost, + line_total=line + ) + + purchase.total_amount = total + + # Handle payment + pay_amount = decimal.Decimal(str(data.get('payment_amount', 0))) + purchase.paid_amount = pay_amount + purchase.balance_due = total - pay_amount + purchase.status = 'paid' if purchase.balance_due <= 0 else ('partial' if pay_amount > 0 else 'unpaid') + purchase.save() + + if pay_amount > 0: + PurchasePayment.objects.create( + purchase=purchase, + amount=pay_amount, + payment_method_id=data.get('payment_method_id'), + created_by=request.user + ) + + return JsonResponse({'success': True, 'purchase_id': purchase.id}) + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}) + +@login_required +def purchase_detail(request, pk): + purchase = get_object_or_404(Purchase, pk=pk) + settings = SystemSetting.objects.first() + return render(request, 'core/purchase_detail.html', { + 'purchase': purchase, + 'settings': settings, + 'payment_methods': PaymentMethod.objects.filter(is_active=True) + }) + +@login_required +def delete_purchase(request, pk): + get_object_or_404(Purchase, pk=pk).delete() + messages.success(request, "Purchase deleted") + return redirect('purchases') + +@login_required +def edit_purchase(request, pk): + # Stub for now + return redirect('purchases') # --- Quotations --- @login_required -def quotations(request): return render(request, 'core/quotations.html') +def quotations(request): + quotations = Quotation.objects.all().order_by('-created_at') + return render(request, 'core/quotations.html', {'quotations': quotations}) + @login_required -def quotation_create(request): return render(request, 'core/quotation_create.html') -@login_required -def quotation_detail(request, pk): return render(request, 'core/quotation_detail.html') -@login_required -def convert_quotation_to_invoice(request, pk): return redirect('invoices') -@login_required -def delete_quotation(request, pk): return redirect('quotations') +def quotation_create(request): + return render(request, 'core/quotation_create.html', { + 'customers': Customer.objects.all(), + 'products': Product.objects.all(), + 'site_settings': SystemSetting.objects.first() + }) + @csrf_exempt -def create_quotation_api(request): return JsonResponse({'success': True}) +@login_required +def create_quotation_api(request): + if request.method != 'POST': return JsonResponse({'success': False}) + try: + data = json.loads(request.body) + with transaction.atomic(): + q = Quotation.objects.create( + customer_id=data.get('customer_id'), + total_amount=0, + created_by=request.user, + notes=data.get('notes', ''), + quotation_number=f"QT-{timezone.now().strftime('%Y%m%d%H%M%S')}" + ) + total = 0 + for item in data.get('items', []): + qty = decimal.Decimal(str(item['quantity'])) + price = decimal.Decimal(str(item['price'])) + line = qty * price + total += line + QuotationItem.objects.create(quotation=q, product_id=item['id'], quantity=qty, unit_price=price, line_total=line) + q.total_amount = total + q.save() + return JsonResponse({'success': True, 'quotation_id': q.id}) + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}) + +@login_required +def quotation_detail(request, pk): + quotation = get_object_or_404(Quotation, pk=pk) + return render(request, 'core/quotation_detail.html', { + 'quotation': quotation, + 'settings': SystemSetting.objects.first(), + 'amount_in_words': number_to_words_en(quotation.total_amount) + }) + +@login_required +def convert_quotation_to_invoice(request, pk): + # Logic to convert quotation to sale would go here + # For now, just stub it + messages.info(request, "Conversion logic not yet fully implemented") + return redirect('quotations') + +@login_required +def delete_quotation(request, pk): + get_object_or_404(Quotation, pk=pk).delete() + return redirect('quotations') # --- Invoices (Sales) --- @login_required -def invoice_create(request): return render(request, 'core/invoice_create.html') +def invoice_create(request): + settings = SystemSetting.objects.first() + context = { + 'products': Product.objects.filter(is_active=True), + 'customers': Customer.objects.all(), + 'payment_methods': PaymentMethod.objects.filter(is_active=True), + 'site_settings': settings, + 'decimal_places': settings.decimal_places if settings else 3 + } + return render(request, 'core/invoice_create.html', context) + @login_required -def invoice_detail(request, pk): return render(request, 'core/invoice_detail.html') +def invoice_detail(request, pk): + sale = get_object_or_404(Sale, pk=pk) + settings = SystemSetting.objects.first() + amount_in_words = number_to_words_en(sale.total_amount) + return render(request, 'core/invoice_detail.html', { + 'sale': sale, + 'settings': settings, + 'amount_in_words': amount_in_words, + 'payment_methods': PaymentMethod.objects.filter(is_active=True) + }) + @login_required -def add_sale_payment(request, pk): return redirect('invoices') -@login_required -def delete_sale(request, pk): return redirect('invoices') -@login_required -def sale_receipt(request, pk): return render(request, 'core/sale_receipt.html') -@login_required -def edit_invoice(request, pk): return redirect('invoices') +def delete_sale(request, pk): + get_object_or_404(Sale, pk=pk).delete() + return redirect('invoices') + @csrf_exempt def update_sale_api(request, pk): return JsonResponse({'success': True}) -@login_required -def customer_payments(request): return render(request, 'core/customer_payments.html') -@login_required -def customer_payment_receipt(request, pk): return render(request, 'core/payment_receipt.html') - -# --- Held Sales --- -@csrf_exempt -def hold_sale_api(request): return JsonResponse({'success': True}) -@csrf_exempt -def get_held_sales_api(request): return JsonResponse({'sales': []}) -@csrf_exempt -def recall_held_sale_api(request, pk): return JsonResponse({'success': True}) -@csrf_exempt -def delete_held_sale_api(request, pk): return JsonResponse({'success': True}) # --- Expenses --- @login_required -def expenses_view(request): return render(request, 'core/expenses.html') +def expenses_view(request): + expenses = Expense.objects.all().order_by('-date') + return render(request, 'core/expenses.html', { + 'expenses': expenses, + 'categories': ExpenseCategory.objects.all(), + 'payment_methods': PaymentMethod.objects.all() + }) + @login_required -def expense_create_view(request): return redirect('expenses') +def expense_create_view(request): + if request.method == 'POST': + form = ExpenseForm(request.POST, request.FILES) + if form.is_valid(): + form.save() + messages.success(request, "Expense added") + return redirect('expenses') + return redirect('expenses') + @login_required def expense_edit_view(request, pk): return redirect('expenses') @login_required -def expense_delete_view(request, pk): return redirect('expenses') +def expense_delete_view(request, pk): + get_object_or_404(Expense, pk=pk).delete() + return redirect('expenses') @login_required def expense_categories_view(request): return render(request, 'core/expense_categories.html') @login_required @@ -544,21 +874,223 @@ def test_whatsapp_connection(request): return JsonResponse({'success': True}) # --- LPO --- @login_required -def lpo_list(request): return render(request, 'core/lpo_list.html') +def lpo_list(request): return render(request, 'core/lpo_list.html', {'lpos': PurchaseOrder.objects.all()}) @login_required -def lpo_create(request): return render(request, 'core/lpo_create.html') +def lpo_create(request): + return render(request, 'core/lpo_create.html', { + 'suppliers': Supplier.objects.all(), + 'products': Product.objects.all(), + 'site_settings': SystemSetting.objects.first() + }) + +@csrf_exempt @login_required -def lpo_detail(request, pk): return render(request, 'core/lpo_detail.html') +def create_lpo_api(request): + if request.method != 'POST': return JsonResponse({'success': False}) + try: + data = json.loads(request.body) + with transaction.atomic(): + lpo = PurchaseOrder.objects.create( + supplier_id=data.get('supplier_id'), + total_amount=0, + created_by=request.user, + lpo_number=f"LPO-{timezone.now().strftime('%Y%m%d%H%M%S')}" + ) + total = 0 + for item in data.get('items', []): + qty = decimal.Decimal(str(item['quantity'])) + cost = decimal.Decimal(str(item.get('price', 0))) + line = qty * cost + total += line + PurchaseOrderItem.objects.create(purchase_order=lpo, product_id=item['id'], quantity=qty, cost_price=cost, line_total=line) + lpo.total_amount = total + lpo.save() + return JsonResponse({'success': True, 'lpo_id': lpo.id}) + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}) + +@login_required +def lpo_detail(request, pk): + lpo = get_object_or_404(PurchaseOrder, pk=pk) + return render(request, 'core/lpo_detail.html', { + 'lpo': lpo, + 'settings': SystemSetting.objects.first() + }) + @login_required def convert_lpo_to_purchase(request, pk): return redirect('lpo_list') @login_required -def lpo_delete(request, pk): return redirect('lpo_list') +def lpo_delete(request, pk): + get_object_or_404(PurchaseOrder, pk=pk).delete() + return redirect('lpo_list') + +# --- Sales Returns --- +@login_required +def sales_returns(request): return render(request, 'core/sales_returns.html', {'returns': SaleReturn.objects.all()}) +@login_required +def sale_return_create(request): return render(request, 'core/sale_return_create.html') +@login_required +def sale_return_detail(request, pk): + sale_return = get_object_or_404(SaleReturn, pk=pk) + settings = SystemSetting.objects.first() + return render(request, 'core/sale_return_detail.html', { + 'sale_return': sale_return, + 'settings': settings + }) +@login_required +def delete_sale_return(request, pk): return redirect('sales_returns') @csrf_exempt -def create_lpo_api(request): return JsonResponse({'success': True}) +def create_sale_return_api(request): return JsonResponse({'success': True}) + +# --- Purchase Returns --- +@login_required +def purchase_returns(request): return render(request, 'core/purchase_returns.html', {'returns': PurchaseReturn.objects.all()}) +@login_required +def purchase_return_create(request): return render(request, 'core/purchase_return_create.html') +@login_required +def purchase_return_detail(request, pk): + purchase_return = get_object_or_404(PurchaseReturn, pk=pk) + settings = SystemSetting.objects.first() + return render(request, 'core/purchase_return_detail.html', { + 'purchase_return': purchase_return, + 'settings': settings + }) +@login_required +def delete_purchase_return(request, pk): return redirect('purchase_returns') +@csrf_exempt +def create_purchase_return_api(request): return JsonResponse({'success': True}) + +# --- Other Stubs --- +@login_required +def customer_statement(request): return render(request, 'core/customer_statement.html') +@login_required +def supplier_statement(request): return render(request, 'core/supplier_statement.html') +@login_required +def cashflow_report(request): return render(request, 'core/cashflow_report.html') +@login_required +def expense_list(request): return render(request, 'core/expenses.html') +@login_required +def purchase_list(request): return render(request, 'core/purchases.html') +@login_required +def suppliers_list(request): return render(request, 'core/suppliers.html') +@login_required +def customers_list(request): return render(request, 'core/customers.html') +@login_required +def add_device(request): return redirect('settings') +@login_required +def edit_device(request, pk): return redirect('settings') +@login_required +def delete_device(request, pk): return redirect('settings') +@csrf_exempt +def pos_sync_update(request): return JsonResponse({'status': 'ok'}) +@csrf_exempt +def pos_sync_state(request): return JsonResponse({'state': {}}) +@login_required +def customer_display(request): return render(request, 'core/customer_display.html') +@login_required +def supplier_payments(request): return render(request, 'core/supplier_payments.html') +@csrf_exempt +def update_purchase_api(request, pk): return JsonResponse({'success': True}) + +@login_required +def add_sale_payment(request, pk): + sale = get_object_or_404(Sale, pk=pk) + if request.method == 'POST': + try: + amount = decimal.Decimal(request.POST.get('amount', 0)) + payment_method_id = request.POST.get('payment_method_id') + notes = request.POST.get('notes', '') + + if amount > 0: + with transaction.atomic(): + SalePayment.objects.create( + sale=sale, + amount=amount, + payment_method_id=payment_method_id, + created_by=request.user, + notes=notes + ) + + # Recalculate totals + total_paid = SalePayment.objects.filter(sale=sale).aggregate(Sum('amount'))['amount__sum'] or 0 + sale.paid_amount = total_paid + sale.balance_due = sale.total_amount - total_paid + + if sale.balance_due <= 0: + sale.status = 'paid' + elif sale.paid_amount > 0: + sale.status = 'partial' + else: + sale.status = 'unpaid' + + sale.save() + messages.success(request, f"Payment of {amount} recorded successfully.") + else: + messages.error(request, "Amount must be greater than 0.") + except Exception as e: + messages.error(request, f"Error recording payment: {e}") + + return redirect('invoices') + +@login_required +def sale_receipt(request, pk): return render(request, 'core/sale_receipt.html') +@login_required +def edit_invoice(request, pk): return redirect('invoices') +@login_required +def customer_payments(request): return render(request, 'core/customer_payments.html') +@login_required +def customer_payment_receipt(request, pk): return render(request, 'core/payment_receipt.html') +@csrf_exempt +def hold_sale_api(request): return JsonResponse({'success': True}) +@csrf_exempt +def get_held_sales_api(request): return JsonResponse({'sales': []}) +@csrf_exempt +def recall_held_sale_api(request, pk): return JsonResponse({'success': True}) +@csrf_exempt +def delete_held_sale_api(request, pk): return JsonResponse({'success': True}) + +@login_required +def add_purchase_payment(request, pk): + purchase = get_object_or_404(Purchase, pk=pk) + if request.method == 'POST': + try: + amount = decimal.Decimal(request.POST.get('amount', 0)) + payment_method_id = request.POST.get('payment_method_id') + notes = request.POST.get('notes', '') + + if amount > 0: + with transaction.atomic(): + PurchasePayment.objects.create( + purchase=purchase, + amount=amount, + payment_method_id=payment_method_id, + created_by=request.user, + notes=notes + ) + + # Recalculate totals + total_paid = PurchasePayment.objects.filter(purchase=purchase).aggregate(Sum('amount'))['amount__sum'] or 0 + purchase.paid_amount = total_paid + purchase.balance_due = purchase.total_amount - total_paid + + if purchase.balance_due <= 0: + purchase.status = 'paid' + elif purchase.paid_amount > 0: + purchase.status = 'partial' + else: + purchase.status = 'unpaid' + + purchase.save() + messages.success(request, f"Payment of {amount} recorded successfully.") + else: + messages.error(request, "Amount must be greater than 0.") + except Exception as e: + messages.error(request, f"Error recording payment: {e}") + + return redirect('purchases') + @login_required def cashier_registry(request): return render(request, 'core/cashier_registry.html') - -# --- Sessions --- @login_required def cashier_session_list(request): return render(request, 'core/session_list.html') @login_required @@ -567,53 +1099,19 @@ def start_session(request): return redirect('pos') def close_session(request): return redirect('index') @login_required def session_detail(request, pk): return render(request, 'core/session_detail.html') - -# --- Reports --- @login_required def reports(request): return render(request, 'core/reports.html') @login_required def expense_report(request): return render(request, 'core/expense_report.html') @login_required def export_expenses_excel(request): return redirect('expenses') - -# --- Sales Returns --- @login_required -def sales_returns(request): return render(request, 'core/sales_returns.html') +def profile_view(request): return render(request, 'core/profile.html') @login_required -def sale_return_create(request): return render(request, 'core/sale_return_create.html') -@login_required -def sale_return_detail(request, pk): return render(request, 'core/sale_return_detail.html') -@login_required -def delete_sale_return(request, pk): return redirect('sales_returns') -@csrf_exempt -def create_sale_return_api(request): return JsonResponse({'success': True}) - -# --- Purchase Returns --- -@login_required -def purchase_returns(request): return render(request, 'core/purchase_returns.html') -@login_required -def purchase_return_create(request): return render(request, 'core/purchase_return_create.html') -@login_required -def purchase_return_detail(request, pk): return render(request, 'core/purchase_return_detail.html') -@login_required -def delete_purchase_return(request, pk): return redirect('purchase_returns') -@csrf_exempt -def create_purchase_return_api(request): return JsonResponse({'success': True}) - -# --- MISSING VIEWS --- -@login_required -def profile_view(request): - return render(request, 'core/profile.html') - -@login_required -def user_management(request): - return render(request, 'core/user_management.html') - +def user_management(request): return render(request, 'core/user_management.html') @csrf_exempt @login_required -def group_details_api(request, pk): - return JsonResponse({'success': True, 'group': {}}) - +def group_details_api(request, pk): return JsonResponse({'success': True, 'group': {}}) @csrf_exempt @login_required def search_customers_api(request): diff --git a/patch_payments_v2.py b/patch_payments_v2.py new file mode 100644 index 0000000..82e800b --- /dev/null +++ b/patch_payments_v2.py @@ -0,0 +1,211 @@ +import os +import decimal +from django.db import transaction +from django.db.models import Sum + +file_path = 'core/views.py' +with open(file_path, 'r') as f: + content = f.read() + +# 1. Update invoice_list +old_invoice_list = """@login_required +def invoice_list(request): + sales = Sale.objects.all().order_by('-created_at') + paginator = Paginator(sales, 25) + return render(request, 'core/invoices.html', { + 'sales': paginator.get_page(request.GET.get('page')), + 'customers': Customer.objects.all(), + 'site_settings': SystemSetting.objects.first() + })""" + +new_invoice_list = """@login_required +def invoice_list(request): + sales = Sale.objects.all().order_by('-created_at') + paginator = Paginator(sales, 25) + return render(request, 'core/invoices.html', { + 'sales': paginator.get_page(request.GET.get('page')), + 'customers': Customer.objects.all(), + 'site_settings': SystemSetting.objects.first(), + 'payment_methods': PaymentMethod.objects.filter(is_active=True) + })""" + +if old_invoice_list in content: + content = content.replace(old_invoice_list, new_invoice_list) +else: + print("Could not find old_invoice_list") + +# 2. Update purchases +old_purchases = """@login_required +def purchases(request): + purchases = Purchase.objects.all().order_by('-created_at') + paginator = Paginator(purchases, 25) + return render(request, 'core/purchases.html', {'purchases': paginator.get_page(request.GET.get('page'))})""" + +new_purchases = """@login_required +def purchases(request): + purchases = Purchase.objects.all().order_by('-created_at') + paginator = Paginator(purchases, 25) + return render(request, 'core/purchases.html', { + 'purchases': paginator.get_page(request.GET.get('page')), + 'payment_methods': PaymentMethod.objects.filter(is_active=True) + })""" + +if old_purchases in content: + content = content.replace(old_purchases, new_purchases) +else: + print("Could not find old_purchases") + +# 3. Update invoice_detail +old_invoice_detail = """@login_required +def invoice_detail(request, pk): + sale = get_object_or_404(Sale, pk=pk) + settings = SystemSetting.objects.first() + amount_in_words = number_to_words_en(sale.total_amount) + return render(request, 'core/invoice_detail.html', { + 'sale': sale, + 'settings': settings, + 'amount_in_words': amount_in_words + })""" + +new_invoice_detail = """@login_required +def invoice_detail(request, pk): + sale = get_object_or_404(Sale, pk=pk) + settings = SystemSetting.objects.first() + amount_in_words = number_to_words_en(sale.total_amount) + return render(request, 'core/invoice_detail.html', { + 'sale': sale, + 'settings': settings, + 'amount_in_words': amount_in_words, + 'payment_methods': PaymentMethod.objects.filter(is_active=True) + })""" + +if old_invoice_detail in content: + content = content.replace(old_invoice_detail, new_invoice_detail) +else: + print("Could not find old_invoice_detail") + +# 4. Update purchase_detail +old_purchase_detail = """@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 + })""" + +new_purchase_detail = """@login_required +def purchase_detail(request, pk): + purchase = get_object_or_404(Purchase, pk=pk) + settings = SystemSetting.objects.first() + return render(request, 'core/purchase_detail.html', { + 'purchase': purchase, + 'settings': settings, + 'payment_methods': PaymentMethod.objects.filter(is_active=True) + })""" + +if old_purchase_detail in content: + content = content.replace(old_purchase_detail, new_purchase_detail) +else: + print("Could not find old_purchase_detail") + +# 5. Replace add_sale_payment stub +old_add_sale_payment = """@login_required +def add_sale_payment(request, pk): return redirect('invoices')""" + +new_add_sale_payment = """@login_required +def add_sale_payment(request, pk): + sale = get_object_or_404(Sale, pk=pk) + if request.method == 'POST': + try: + amount = decimal.Decimal(request.POST.get('amount', 0)) + payment_method_id = request.POST.get('payment_method_id') + notes = request.POST.get('notes', '') + + if amount > 0: + with transaction.atomic(): + SalePayment.objects.create( + sale=sale, + amount=amount, + payment_method_id=payment_method_id, + created_by=request.user, + notes=notes + ) + + # Recalculate totals + total_paid = SalePayment.objects.filter(sale=sale).aggregate(Sum('amount'))['amount__sum'] or 0 + sale.paid_amount = total_paid + sale.balance_due = sale.total_amount - total_paid + + if sale.balance_due <= 0: + sale.status = 'paid' + elif sale.paid_amount > 0: + sale.status = 'partial' + else: + sale.status = 'unpaid' + + sale.save() + messages.success(request, f"Payment of {amount} recorded successfully.") + else: + messages.error(request, "Amount must be greater than 0.") + except Exception as e: + messages.error(request, f"Error recording payment: {e}") + + return redirect('invoices')""" + +if old_add_sale_payment in content: + content = content.replace(old_add_sale_payment, new_add_sale_payment) +else: + print("Could not find old_add_sale_payment") + +# 6. Replace add_purchase_payment stub +old_add_purchase_payment = """@login_required +def add_purchase_payment(request, pk): return redirect('purchases')""" + +new_add_purchase_payment = """@login_required +def add_purchase_payment(request, pk): + purchase = get_object_or_404(Purchase, pk=pk) + if request.method == 'POST': + try: + amount = decimal.Decimal(request.POST.get('amount', 0)) + payment_method_id = request.POST.get('payment_method_id') + notes = request.POST.get('notes', '') + + if amount > 0: + with transaction.atomic(): + PurchasePayment.objects.create( + purchase=purchase, + amount=amount, + payment_method_id=payment_method_id, + created_by=request.user, + notes=notes + ) + + # Recalculate totals + total_paid = PurchasePayment.objects.filter(purchase=purchase).aggregate(Sum('amount'))['amount__sum'] or 0 + purchase.paid_amount = total_paid + purchase.balance_due = purchase.total_amount - total_paid + + if purchase.balance_due <= 0: + purchase.status = 'paid' + elif purchase.paid_amount > 0: + purchase.status = 'partial' + else: + purchase.status = 'unpaid' + + purchase.save() + messages.success(request, f"Payment of {amount} recorded successfully.") + else: + messages.error(request, "Amount must be greater than 0.") + except Exception as e: + messages.error(request, f"Error recording payment: {e}") + + return redirect('purchases')""" + +if old_add_purchase_payment in content: + content = content.replace(old_add_purchase_payment, new_add_purchase_payment) +else: + print("Could not find old_add_purchase_payment") + +with open(file_path, 'w') as f: + f.write(content)