diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 55cf2dd..7af4195 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 6c77f41..6495a9b 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 79a6af6..511ba99 100644 --- a/core/models.py +++ b/core/models.py @@ -42,6 +42,7 @@ class Product(models.Model): expiry_date = models.DateField(_("Expiry Date"), null=True, blank=True) image = models.FileField(_("Product Image"), upload_to="product_images/", blank=True, null=True) is_active = models.BooleanField(_("Active"), default=True) + is_service = models.BooleanField(_("Is Service"), default=False) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): @@ -65,6 +66,7 @@ class Customer(models.Model): address = models.TextField(_("Address"), blank=True) loyalty_points = models.DecimalField(_("Loyalty Points"), max_digits=15, decimal_places=2, default=0) loyalty_tier = models.ForeignKey(LoyaltyTier, on_delete=models.SET_NULL, null=True, blank=True, related_name="customers") + created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name @@ -95,6 +97,7 @@ class Supplier(models.Model): name = models.CharField(_("Name"), max_length=200) contact_person = models.CharField(_("Contact Person"), max_length=200, blank=True) phone = models.CharField(_("Phone"), max_length=20, blank=True) + created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name @@ -374,11 +377,10 @@ class PurchaseReturnItem(models.Model): return f"{self.product.name_en} x {self.quantity}" class HeldSale(models.Model): - customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="held_sales") - cart_data = models.JSONField(_("Cart Data")) - total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) - notes = models.TextField(_("Notes"), blank=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="held_sales") + cart_data = models.TextField(_("Cart Data")) + customer_name = models.CharField(_("Customer Name"), max_length=200, blank=True) + note = models.TextField(_("Note"), blank=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): @@ -483,10 +485,10 @@ class CashierSession(models.Model): @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: - UserProfile.objects.create(user=instance) + UserProfile.objects.get_or_create(user=instance) @receiver(post_save, sender=User) def save_user_profile(sender, instance, **kwargs): - if not hasattr(instance, 'profile'): - UserProfile.objects.create(user=instance) - instance.profile.save() \ No newline at end of file + UserProfile.objects.get_or_create(user=instance) + if hasattr(instance, 'profile'): + instance.profile.save() \ No newline at end of file diff --git a/core/views.py b/core/views.py index 41af225..84ef718 100644 --- a/core/views.py +++ b/core/views.py @@ -9,6 +9,7 @@ from django.db import transaction, models from django.db.models import Sum, Count, F, Q from django.utils import timezone from django.core.paginator import Paginator +from django.utils.text import slugify import json import decimal import datetime @@ -25,20 +26,14 @@ from .views_import import import_categories, import_suppliers, import_products @login_required def index(request): - # 1. Basic Counts total_sales_amount = Sale.objects.aggregate(total=Sum('total_amount'))['total'] or 0 total_sales_count = Sale.objects.count() total_products = Product.objects.count() total_customers = Customer.objects.count() - - # New: Receivables and Payables total_receivables = Sale.objects.aggregate(total=Sum('balance_due'))['total'] or 0 total_payables = Purchase.objects.aggregate(total=Sum('balance_due'))['total'] or 0 - # 2. Charts Data today = timezone.now().date() - - # A. Monthly Sales (Current Year) current_year = today.year monthly_sales = (Sale.objects.filter(created_at__year=current_year) .annotate(month=models.functions.ExtractMonth('created_at')) @@ -48,7 +43,6 @@ def index(request): monthly_labels = [] monthly_data = [] - # Initialize 12 months with 0 months_map = {i: 0 for i in range(1, 13)} for entry in monthly_sales: months_map[entry['month']] = float(entry['total']) @@ -58,7 +52,6 @@ def index(request): monthly_labels.append(calendar.month_abbr[i]) monthly_data.append(months_map[i]) - # B. Daily Sales (Last 7 Days) seven_days_ago = today - timedelta(days=6) daily_sales = (Sale.objects.filter(created_at__date__gte=seven_days_ago) .annotate(day=models.functions.ExtractDay('created_at')) @@ -68,7 +61,6 @@ def index(request): chart_labels = [] chart_data = [] - # Map dates to ensure continuity date_map = {} current_date = seven_days_ago while current_date <= today: @@ -82,7 +74,6 @@ def index(request): chart_labels.append(date_key.strftime('%d %b')) chart_data.append(date_map[date_key]) - # C. Sales by Category category_sales = (SaleItem.objects.values('product__category__name_en') .annotate(total=Sum('line_total')) .order_by('-total')[:5]) @@ -90,7 +81,6 @@ def index(request): category_labels = [item['product__category__name_en'] for item in category_sales] category_data = [float(item['total']) for item in category_sales] - # D. Payment Methods payment_stats = (SalePayment.objects.values('payment_method_name') .annotate(total=Sum('amount')) .order_by('-total')) @@ -98,23 +88,13 @@ def index(request): payment_labels = [item['payment_method_name'] if item['payment_method_name'] else 'Unknown' for item in payment_stats] payment_data = [float(item['total']) for item in payment_stats] - # 3. Top 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_rev')[:5]) - # 4. Recent Sales recent_sales = Sale.objects.select_related('customer').order_by('-created_at')[:5] - - # 5. Low Stock Alert low_stock_products = Product.objects.filter(is_active=True, stock_quantity__lte=F('min_stock_level'))[:5] - - # 6. Expired Products (if applicable) - expired_products = Product.objects.filter( - is_active=True, - has_expiry=True, - expiry_date__lt=today - )[:5] + expired_products = Product.objects.filter(is_active=True, has_expiry=True, expiry_date__lt=today)[:5] context = { 'total_sales_amount': total_sales_amount, @@ -140,981 +120,536 @@ def index(request): @login_required def inventory(request): - products = Product.objects.filter(is_active=True) + products = Product.objects.filter(is_active=True).select_related('category', 'unit', 'supplier') categories = Category.objects.all() units = Unit.objects.all() suppliers = Supplier.objects.all() - # Filter by category category_id = request.GET.get('category') if category_id: products = products.filter(category_id=category_id) - - # Search query = request.GET.get('q') if query: - products = products.filter( - Q(name_en__icontains=query) | - Q(name_ar__icontains=query) | - Q(sku__icontains=query) - ) + products = products.filter(Q(name_en__icontains=query) | Q(name_ar__icontains=query) | Q(sku__icontains=query)) + + today = timezone.now().date() + expiring_soon_products = Product.objects.filter(is_active=True, has_expiry=True, expiry_date__lte=today + timedelta(days=30), expiry_date__gte=today) + expired_products = Product.objects.filter(is_active=True, has_expiry=True, expiry_date__lt=today) + + paginator = Paginator(products, 20) + page_number = request.GET.get('page') + page_obj = paginator.get_page(page_number) context = { - 'products': products, + 'products': page_obj, 'categories': categories, 'units': units, 'suppliers': suppliers, + 'expiring_soon_products': expiring_soon_products, + 'expired_products': expired_products, + 'site_settings': SystemSetting.objects.first(), } return render(request, 'core/inventory.html', context) @login_required def customers(request): customers_list = Customer.objects.all().order_by('-created_at') - query = request.GET.get('q') if query: - customers_list = customers_list.filter( - Q(name__icontains=query) | - Q(phone__icontains=query) | - Q(email__icontains=query) - ) - - paginator = Paginator(customers_list, 10) - page_number = request.GET.get('page') - page_obj = paginator.get_page(page_number) - - return render(request, 'core/customers.html', {'page_obj': page_obj}) + customers_list = customers_list.filter(Q(name__icontains=query) | Q(phone__icontains=query) | Q(email__icontains=query)) + paginator = Paginator(customers_list, 20) + page_obj = paginator.get_page(request.GET.get('page')) + return render(request, 'core/customers.html', {'customers': page_obj}) @login_required def suppliers(request): suppliers_list = Supplier.objects.all().order_by('-created_at') - query = request.GET.get('q') if query: - suppliers_list = suppliers_list.filter( - Q(name__icontains=query) | - Q(contact_person__icontains=query) | - Q(phone__icontains=query) - ) - - paginator = Paginator(suppliers_list, 10) - page_number = request.GET.get('page') - page_obj = paginator.get_page(page_number) - - return render(request, 'core/suppliers.html', {'page_obj': page_obj}) + suppliers_list = suppliers_list.filter(Q(name__icontains=query) | Q(contact_person__icontains=query) | Q(phone__icontains=query)) + paginator = Paginator(suppliers_list, 20) + page_obj = paginator.get_page(request.GET.get('page')) + return render(request, 'core/suppliers.html', {'suppliers': page_obj}) @login_required def purchases(request): purchases_list = Purchase.objects.all().select_related('supplier').order_by('-created_at') - start_date = request.GET.get('start_date') end_date = request.GET.get('end_date') supplier_id = request.GET.get('supplier') - - if start_date: - purchases_list = purchases_list.filter(created_at__date__gte=start_date) - if end_date: - purchases_list = purchases_list.filter(created_at__date__lte=end_date) - if supplier_id: - purchases_list = purchases_list.filter(supplier_id=supplier_id) - - suppliers = Supplier.objects.all() - - paginator = Paginator(purchases_list, 10) - page_number = request.GET.get('page') - page_obj = paginator.get_page(page_number) - - return render(request, 'core/purchases.html', { - 'page_obj': page_obj, - 'suppliers': suppliers - }) + if start_date: purchases_list = purchases_list.filter(created_at__date__gte=start_date) + if end_date: purchases_list = purchases_list.filter(created_at__date__lte=end_date) + if supplier_id: purchases_list = purchases_list.filter(supplier_id=supplier_id) + paginator = Paginator(purchases_list, 20) + page_obj = paginator.get_page(request.GET.get('page')) + return render(request, 'core/purchases.html', {'page_obj': page_obj, 'suppliers': Supplier.objects.all()}) @login_required -def reports(request): - return render(request, 'core/reports.html') - +def reports(request): return render(request, 'core/reports.html') @login_required -def customer_statement(request): - return render(request, 'core/reports.html') # Placeholder - +def customer_statement(request): return render(request, 'core/reports.html') @login_required -def supplier_statement(request): - return render(request, 'core/reports.html') # Placeholder - +def supplier_statement(request): return render(request, 'core/reports.html') @login_required -def cashflow_report(request): - return render(request, 'core/reports.html') # Placeholder +def cashflow_report(request): return render(request, 'core/reports.html') @login_required def settings_view(request): - return render(request, 'core/settings.html') + settings = SystemSetting.objects.first() + payment_methods = PaymentMethod.objects.all() + devices = Device.objects.all() + loyalty_tiers = LoyaltyTier.objects.all() + return render(request, 'core/settings.html', { + 'settings': settings, + 'payment_methods': payment_methods, + 'devices': devices, + 'loyalty_tiers': loyalty_tiers + }) @login_required -def profile_view(request): - return render(request, 'core/profile.html') - +def profile_view(request): return render(request, 'core/profile.html') @login_required def user_management(request): from django.contrib.auth.models import User, Group - users = User.objects.all() - groups = Group.objects.all() - return render(request, 'core/user_management.html', {'users': users, 'groups': groups}) + return render(request, 'core/user_management.html', {'users': User.objects.all(), 'groups': Group.objects.all()}) @login_required def group_details_api(request, pk): - from django.contrib.auth.models import Group, Permission + from django.contrib.auth.models import Group group = get_object_or_404(Group, pk=pk) - permissions = group.permissions.all() - data = { 'id': group.id, 'name': group.name, - 'permissions': [{'id': p.id, 'name': p.name, 'codename': p.codename} for p in permissions] + 'permissions': [{'id': p.id, 'name': p.name, 'codename': p.codename} for p in group.permissions.all()] } return JsonResponse(data) - # ========================================== -# POS & Sales Views +# POS & Sales # ========================================== @login_required def pos(request): - categories = Category.objects.all() - products = Product.objects.filter(is_active=True).select_related('category', 'unit') - customers = Customer.objects.all() - payment_methods = PaymentMethod.objects.filter(is_active=True) - - # Check for active session - session = CashierSession.objects.filter(user=request.user, end_time__isnull=True).last() - - # Retrieve held sales - held_sales = HeldSale.objects.filter(created_by=request.user).order_by('-created_at') - + session = CashierSession.objects.filter(user=request.user, status='active').last() context = { - 'categories': categories, - 'products': products, - 'customers': customers, - 'payment_methods': payment_methods, + 'categories': Category.objects.all(), + 'products': Product.objects.filter(is_active=True).select_related('category', 'unit'), + 'customers': Customer.objects.all(), + 'payment_methods': PaymentMethod.objects.filter(is_active=True), 'session': session, - 'held_sales': held_sales, + 'held_sales': HeldSale.objects.filter(created_by=request.user).order_by('-created_at'), } return render(request, 'core/pos.html', context) @login_required -def customer_display(request): - return render(request, 'core/customer_display.html') +def customer_display(request): return render(request, 'core/customer_display.html') @csrf_exempt @login_required def create_sale_api(request): - if request.method != 'POST': - return JsonResponse({'success': False, 'message': 'Invalid method'}, status=405) - + if request.method != 'POST': return JsonResponse({'success': False}, status=405) try: data = json.loads(request.body) - customer_id = data.get('customer_id') items = data.get('items', []) payments = data.get('payments', []) - # --- Handle Single Payment Payload (from Invoice Create & Simple POS) --- if not payments: - payment_type = data.get('payment_type', 'cash') paid_amount = decimal.Decimal(str(data.get('paid_amount', 0))) - payment_method_id = data.get('payment_method_id') - - if payment_type == 'credit': - # No payment - pass - elif paid_amount > 0: - # Fetch method name - method_name = "Cash" - if payment_method_id: - try: - pm = PaymentMethod.objects.get(id=payment_method_id) - method_name = pm.name_en # Or whatever field we want to store - except: - pass - - payments.append({ - 'method': method_name, - 'amount': paid_amount - }) - # ---------------------------------------------------------------------- - - discount = decimal.Decimal(str(data.get('discount', 0))) - notes = data.get('notes', '') - invoice_number = data.get('invoice_number', '') - due_date = data.get('due_date') - - if not items: - return JsonResponse({'success': False, 'message': 'No items in cart'}, status=400) - - # Validate Session - session = CashierSession.objects.filter(user=request.user, end_time__isnull=True).last() - # if not session: ... (Optional check) + if paid_amount > 0: + method_name = "Cash" + pm_id = data.get('payment_method_id') + if pm_id: + try: method_name = PaymentMethod.objects.get(id=pm_id).name_en + except: pass + payments.append({'method': method_name, 'amount': paid_amount}) with transaction.atomic(): - customer = None - if customer_id: - customer = Customer.objects.get(id=customer_id) - + customer = Customer.objects.get(id=customer_id) if customer_id else None sale = Sale.objects.create( created_by=request.user, customer=customer, - invoice_number=invoice_number, - total_amount=0, # Will calculate - discount=discount, - notes=notes, - payment_status='Pending' + invoice_number=data.get('invoice_number', ''), + total_amount=0, + discount=decimal.Decimal(str(data.get('discount', 0))), + notes=data.get('notes', ''), + status='unpaid' ) - - if due_date: - sale.due_date = due_date + if data.get('due_date'): sale.due_date = data.get('due_date') subtotal = decimal.Decimal(0) - vat_amount = decimal.Decimal(0) # Track total VAT - + vat_amount = decimal.Decimal(0) for item in items: product = Product.objects.select_for_update().get(id=item['id']) qty = decimal.Decimal(str(item['quantity'])) - price = decimal.Decimal(str(item['price'])) + price = decimal.Decimal(str(item['price'])) - # Check System Setting for Zero Stock setting = SystemSetting.objects.first() - allow_zero = setting.allow_zero_stock_sales if setting else False - - if not product.is_service and product.stock_quantity < qty and not allow_zero: + if not product.is_service and product.stock_quantity < qty and (not setting or not setting.allow_zero_stock_sales): raise Exception(f"Insufficient stock for {product.name_en}") line_total = price * qty subtotal += line_total - - # Calculate VAT for this line - # Assuming price includes VAT? Or excludes? - # POS usually excludes or includes based on settings. - # Let's assume price is Unit Price *before* VAT if we add VAT separately? - # Or let's assume simple logic: Line Total is what user sees. - # But we should probably calculate VAT based on product.vat - item_vat = line_total * (product.vat / 100) - vat_amount += item_vat - - SaleItem.objects.create( - sale=sale, - product=product, - quantity=qty, - unit_price=price, # Fixed field name - line_total=line_total - ) - - # Update stock + vat_amount += line_total * (product.vat / 100) + SaleItem.objects.create(sale=sale, product=product, quantity=qty, unit_price=price, line_total=line_total) if not product.is_service: product.stock_quantity -= qty product.save() - # Recalculate Totals sale.subtotal = subtotal sale.vat_amount = vat_amount - sale.total_amount = subtotal + vat_amount - discount + sale.total_amount = subtotal + vat_amount - sale.discount - # Process Payments - paid_amount = decimal.Decimal(0) + paid = decimal.Decimal(0) for p in payments: - amount = decimal.Decimal(str(p['amount'])) - method_name = p['method'] - - SalePayment.objects.create( - sale=sale, - payment_method_name=method_name, - amount=amount, - created_by=request.user - ) - paid_amount += amount - - sale.paid_amount = paid_amount - sale.balance_due = sale.total_amount - paid_amount - - if sale.balance_due <= 0: - sale.payment_status = 'Paid' - elif paid_amount > 0: - sale.payment_status = 'Partial' - else: - sale.payment_status = 'Unpaid' - + amt = decimal.Decimal(str(p['amount'])) + SalePayment.objects.create(sale=sale, payment_method_name=p['method'], amount=amt, created_by=request.user) + paid += amt + sale.paid_amount = paid + sale.balance_due = sale.total_amount - paid + sale.status = 'paid' if sale.balance_due <= 0 else ('partial' if paid > 0 else 'unpaid') sale.save() - - return JsonResponse({'success': True, 'sale_id': sale.id, 'message': 'Sale created successfully'}) - + return JsonResponse({'success': True, 'sale_id': sale.id}) except Exception as e: - import traceback - traceback.print_exc() return JsonResponse({'success': False, 'message': str(e)}, status=500) -@csrf_exempt -@login_required -def update_sale_api(request, pk): - return JsonResponse({'success': False, 'message': 'Not implemented'}, status=501) - @csrf_exempt @login_required def hold_sale_api(request): - if request.method != 'POST': - return JsonResponse({'success': False, 'message': 'Invalid method'}, status=405) + if request.method != 'POST': return JsonResponse({'success': False}, status=405) try: data = json.loads(request.body) - cart_data = json.dumps(data.get('cart_data', {})) - note = data.get('note', '') - customer_name = data.get('customer_name', '') - HeldSale.objects.create( created_by=request.user, - cart_data=cart_data, - note=note, - customer_name=customer_name + cart_data=json.dumps(data.get('cart_data', {})), + note=data.get('note', ''), + customer_name=data.get('customer_name', '') ) return JsonResponse({'success': True}) - except Exception as e: - return JsonResponse({'success': False, 'message': str(e)}, status=500) + except Exception as e: return JsonResponse({'success': False, 'message': str(e)}, status=500) @login_required def get_held_sales_api(request): sales = HeldSale.objects.filter(created_by=request.user).order_by('-created_at') - data = [] - for s in sales: - data.append({ - 'id': s.id, - 'created_at': s.created_at.strftime('%Y-%m-%d %H:%M'), - 'customer_name': s.customer_name, - 'note': s.note, - 'cart_data': json.loads(s.cart_data) - }) + data = [{'id': s.id, 'created_at': s.created_at.strftime('%Y-%m-%d %H:%M'), 'customer_name': s.customer_name, 'note': s.note, 'cart_data': json.loads(s.cart_data)} for s in sales] return JsonResponse({'sales': data}) @csrf_exempt @login_required def recall_held_sale_api(request, pk): - # Just return the data, maybe delete it or keep it until finalized? - # Usually we delete it after recall or keep it. Let's keep it until explicitly deleted or completed. - held_sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user) - return JsonResponse({ - 'success': True, - 'cart_data': json.loads(held_sale.cart_data), - 'customer_name': held_sale.customer_name, - 'note': held_sale.note - }) + s = get_object_or_404(HeldSale, pk=pk, created_by=request.user) + return JsonResponse({'success': True, 'cart_data': json.loads(s.cart_data), 'customer_name': s.customer_name, 'note': s.note}) @csrf_exempt @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() + get_object_or_404(HeldSale, pk=pk, created_by=request.user).delete() return JsonResponse({'success': True}) -# ========================================== -# Invoice / Quotation / Return Views -# ========================================== - +# Invoices @login_required def invoice_list(request): sales = Sale.objects.select_related('customer', 'created_by').order_by('-created_at') - - # Filter - status = request.GET.get('status') - if status: - sales = sales.filter(payment_status=status) - - start_date = request.GET.get('start_date') - end_date = request.GET.get('end_date') - if start_date: - sales = sales.filter(created_at__date__gte=start_date) - if end_date: - sales = sales.filter(created_at__date__lte=end_date) - - customers = Customer.objects.all() - + if request.GET.get('status'): sales = sales.filter(status=request.GET.get('status')) + if request.GET.get('start_date'): sales = sales.filter(created_at__date__gte=request.GET.get('start_date')) + if request.GET.get('end_date'): sales = sales.filter(created_at__date__lte=request.GET.get('end_date')) paginator = Paginator(sales, 20) - page_number = request.GET.get('page') - page_obj = paginator.get_page(page_number) - - payment_methods = PaymentMethod.objects.filter(is_active=True) - return render(request, 'core/invoices.html', { - 'page_obj': page_obj, - 'sales': page_obj, - 'payment_methods': payment_methods, - 'customers': customers + 'sales': paginator.get_page(request.GET.get('page')), + 'payment_methods': PaymentMethod.objects.filter(is_active=True), + 'customers': Customer.objects.all(), + 'site_settings': SystemSetting.objects.first() }) @login_required def invoice_detail(request, pk): sale = get_object_or_404(Sale, pk=pk) - return render(request, 'core/invoice_detail.html', {'sale': sale}) + return render(request, 'core/invoice_detail.html', {'sale': sale, 'payment_methods': PaymentMethod.objects.filter(is_active=True)}) @login_required def invoice_create(request): - # Retrieve data for the invoice form - products = Product.objects.filter(is_active=True).select_related('category', 'unit') - customers = Customer.objects.all() - payment_methods = PaymentMethod.objects.filter(is_active=True) - site_settings = SystemSetting.objects.first() - - context = { - 'products': products, - 'customers': customers, - 'payment_methods': payment_methods, - 'site_settings': site_settings, - 'decimal_places': site_settings.decimal_places if site_settings else 3, - } - return render(request, 'core/invoice_create.html', context) - -@login_required -def delete_sale(request, pk): - sale = get_object_or_404(Sale, pk=pk) - if request.method == 'POST': - # Restore stock? - with transaction.atomic(): - for item in sale.items.all(): - if not item.product.is_service: - item.product.stock_quantity += item.quantity - item.product.save() - sale.delete() - messages.success(request, "Invoice deleted and stock restored.") - return redirect('invoices') - return render(request, 'core/confirm_delete.html', {'object': sale}) + return render(request, 'core/invoice_create.html', { + 'products': Product.objects.filter(is_active=True).select_related('category', 'unit'), + 'customers': Customer.objects.all(), + 'payment_methods': PaymentMethod.objects.filter(is_active=True), + 'site_settings': SystemSetting.objects.first() + }) @login_required def add_sale_payment(request, pk): sale = get_object_or_404(Sale, pk=pk) if request.method == 'POST': amount = decimal.Decimal(request.POST.get('amount', 0)) - method = request.POST.get('method') - + pm_id = request.POST.get('payment_method_id') if amount > 0: - SalePayment.objects.create( - sale=sale, - payment_method_name=method, - amount=amount - ) - sale.paid_amount += amount - sale.balance_due = sale.total_amount - sale.paid_amount - if sale.balance_due <= 0: - sale.payment_status = 'Paid' - elif sale.paid_amount > 0: - sale.payment_status = 'Partial' - sale.save() - messages.success(request, "Payment added.") + method_name = "Cash" + if pm_id: + try: method_name = PaymentMethod.objects.get(id=pm_id).name_en + except: pass + SalePayment.objects.create(sale=sale, payment_method_name=method_name, amount=amount, created_by=request.user, notes=request.POST.get('notes', '')) + sale.update_balance() + messages.success(request, _("Payment recorded.")) return redirect('invoice_detail', pk=pk) +@login_required +def delete_sale(request, pk): + sale = get_object_or_404(Sale, pk=pk) + if request.method == 'POST': + with transaction.atomic(): + for item in sale.items.all(): + if not item.product.is_service: + item.product.stock_quantity += item.quantity + item.product.save() + sale.delete() + messages.success(request, _("Invoice deleted.")) + return redirect('invoices') + return render(request, 'core/confirm_delete.html', {'object': sale}) # Quotations @login_required -def quotations(request): - quots = Quotation.objects.all().order_by('-created_at') - return render(request, 'core/quotations.html', {'quotations': quots}) - +def quotations(request): return render(request, 'core/quotations.html', {'quotations': Quotation.objects.all().order_by('-created_at')}) @login_required -def quotation_create(request): - customers = Customer.objects.all() - products = Product.objects.filter(is_active=True) - return render(request, 'core/quotation_create.html', {'customers': customers, 'products': products}) - +def quotation_create(request): return render(request, 'core/quotation_create.html', {'customers': Customer.objects.all(), 'products': Product.objects.filter(is_active=True)}) @login_required -def quotation_detail(request, pk): - quotation = get_object_or_404(Quotation, pk=pk) - return render(request, 'core/quotation_detail.html', {'quotation': quotation}) +def quotation_detail(request, pk): return render(request, 'core/quotation_detail.html', {'quotation': get_object_or_404(Quotation, pk=pk)}) @csrf_exempt @login_required def create_quotation_api(request): - if request.method != 'POST': - return JsonResponse({'success': False}, status=405) + if request.method != 'POST': return JsonResponse({'success': False}, status=405) try: data = json.loads(request.body) - customer_id = data.get('customer_id') - items = data.get('items', []) - - customer = None - if customer_id: - customer = Customer.objects.get(id=customer_id) - - quotation = Quotation.objects.create( - created_by=request.user, - customer=customer, - total_amount=0 - ) - - total = decimal.Decimal(0) - for item in items: - product = Product.objects.get(id=item['id']) - qty = decimal.Decimal(str(item['quantity'])) - price = decimal.Decimal(str(item['price'])) - line = price * qty - total += line - - QuotationItem.objects.create( - quotation=quotation, - product=product, - quantity=qty, - price=price, - line_total=line - ) - - quotation.total_amount = total - quotation.save() - - return JsonResponse({'success': True, 'id': quotation.id}) - except Exception as e: - return JsonResponse({'success': False, 'message': str(e)}) - -@login_required -def delete_quotation(request, pk): - quot = get_object_or_404(Quotation, pk=pk) - if request.method == 'POST': - quot.delete() - messages.success(request, "Quotation deleted.") - return redirect('quotations') - return render(request, 'core/confirm_delete.html', {'object': quot}) + with transaction.atomic(): + q = Quotation.objects.create(created_by=request.user, customer=Customer.objects.get(id=data.get('customer_id')) if data.get('customer_id') else None, total_amount=0) + total = decimal.Decimal(0) + for item in data.get('items', []): + p = Product.objects.get(id=item['id']) + qty, price = decimal.Decimal(str(item['quantity'])), decimal.Decimal(str(item['price'])) + line = qty * price + total += line + QuotationItem.objects.create(quotation=q, product=p, quantity=qty, unit_price=price, line_total=line) + q.total_amount = total; q.save() + return JsonResponse({'success': True, 'id': q.id}) + except Exception as e: return JsonResponse({'success': False, 'message': str(e)}) @login_required def convert_quotation_to_invoice(request, pk): - quot = get_object_or_404(Quotation, pk=pk) - # Logic to convert: create Sale from Quotation - # Check stock first + q = get_object_or_404(Quotation, pk=pk) try: with transaction.atomic(): - sale = Sale.objects.create( - created_by=request.user, - customer=quot.customer, - total_amount=quot.total_amount, - payment_status='Unpaid', - balance_due=quot.total_amount - ) - - for q_item in quot.items.all(): - # Check stock - if not q_item.product.is_service: - # Check system setting - setting = SystemSetting.objects.first() - if not setting or not setting.allow_zero_stock_sales: - if q_item.product.stock_quantity < q_item.quantity: - raise Exception(f"Insufficient stock for {q_item.product.name_en}") - - SaleItem.objects.create( - sale=sale, - product=q_item.product, - quantity=q_item.quantity, - price=q_item.price, - line_total=q_item.line_total - ) - - if not q_item.product.is_service: - q_item.product.stock_quantity -= q_item.quantity - q_item.product.save() - - quot.is_converted = True - quot.save() - messages.success(request, f"Quotation converted to Invoice #{sale.id}") + sale = Sale.objects.create(created_by=request.user, customer=q.customer, total_amount=q.total_amount, status='unpaid', balance_due=q.total_amount) + for item in q.items.all(): + if not item.product.is_service and item.product.stock_quantity < item.quantity: + raise Exception(f"Low stock for {item.product.name_en}") + SaleItem.objects.create(sale=sale, product=item.product, quantity=item.quantity, unit_price=item.unit_price, line_total=item.line_total) + if not item.product.is_service: + item.product.stock_quantity -= item.quantity + item.product.save() + q.status = 'converted'; q.save() return redirect('invoice_detail', pk=sale.id) - except Exception as e: messages.error(request, str(e)) return redirect('quotation_detail', pk=pk) -# Sales Returns +# Inventory @login_required -def sales_returns(request): - returns = SaleReturn.objects.all().order_by('-created_at') - return render(request, 'core/sales_returns.html', {'returns': returns}) - -@login_required -def sale_return_create(request): - # Form to select invoice and items to return - # Simplified: just render page - invoices = Sale.objects.all().order_by('-created_at')[:50] - return render(request, 'core/sale_return_create.html', {'invoices': invoices}) - -@csrf_exempt -@login_required -def create_sale_return_api(request): - if request.method != 'POST': return JsonResponse({'success': False}, status=405) - try: - data = json.loads(request.body) - sale_id = data.get('sale_id') - items = data.get('items', []) - reason = data.get('reason', '') - - sale = Sale.objects.get(id=sale_id) - - with transaction.atomic(): - ret = SaleReturn.objects.create( - sale=sale, - reason=reason, - total_refund_amount=0 - ) - - total_refund = decimal.Decimal(0) - for item in items: - # item is {product_id, quantity, price} - product = Product.objects.get(id=item['product_id']) - qty = decimal.Decimal(str(item['quantity'])) - price = decimal.Decimal(str(item['price'])) # Refund price - - line = qty * price - total_refund += line - - # Restore stock - if not product.is_service: - product.stock_quantity += qty - product.save() - - # Create Return Item (if model exists? Check models) - # Assuming models exist or we just track total. - # Wait, models.py has SaleReturn? Yes. - # Does it have SaleReturnItem? Let's assume yes or add it. - # Previous migration 0009 added it. - pass - - ret.total_refund_amount = total_refund - ret.save() - - return JsonResponse({'success': True}) - except Exception as e: - return JsonResponse({'success': False, 'message': str(e)}) - -@login_required -def sale_return_detail(request, pk): - ret = get_object_or_404(SaleReturn, pk=pk) - return render(request, 'core/sale_return_detail.html', {'return': ret}) - -@login_required -def delete_sale_return(request, pk): - ret = get_object_or_404(SaleReturn, pk=pk) +def add_product(request): if request.method == 'POST': - # Revert stock changes? Complex. - # Usually returns are final. Let's just delete record. - ret.delete() - messages.success(request, "Return record deleted.") - return redirect('sales_returns') - return render(request, 'core/confirm_delete.html', {'object': ret}) + try: + p = Product.objects.create( + category=Category.objects.get(id=request.POST.get('category')), + unit=Unit.objects.get(id=request.POST.get('unit')) if request.POST.get('unit') else None, + supplier=Supplier.objects.get(id=request.POST.get('supplier')) if request.POST.get('supplier') else None, + name_en=request.POST.get('name_en'), name_ar=request.POST.get('name_ar'), sku=request.POST.get('sku'), + cost_price=decimal.Decimal(request.POST.get('cost_price', 0)), price=decimal.Decimal(request.POST.get('price', 0)), + vat=decimal.Decimal(request.POST.get('vat', 0)), opening_stock=decimal.Decimal(request.POST.get('opening_stock', 0)), + stock_quantity=decimal.Decimal(request.POST.get('stock_quantity', 0)), min_stock_level=decimal.Decimal(request.POST.get('min_stock_level', 0)), + has_expiry=request.POST.get('has_expiry') == 'on', expiry_date=request.POST.get('expiry_date') or None, + is_active=request.POST.get('is_active') == 'on', image=request.FILES.get('image'), description=request.POST.get('description', '') + ) + messages.success(request, _("Product added.")); return redirect(reverse('inventory') + '#items') + except Exception as e: messages.error(request, str(e)) + return redirect('inventory') -# Purchases @login_required -def purchase_create(request): - suppliers = Supplier.objects.all() - products = Product.objects.all() - return render(request, 'core/purchase_create.html', {'suppliers': suppliers, 'products': products}) +def edit_product(request, pk): + p = get_object_or_404(Product, pk=pk) + if request.method == 'POST': + try: + p.category = Category.objects.get(id=request.POST.get('category')) + p.unit = Unit.objects.get(id=request.POST.get('unit')) if request.POST.get('unit') else None + p.supplier = Supplier.objects.get(id=request.POST.get('supplier')) if request.POST.get('supplier') else None + p.name_en, p.name_ar, p.sku = request.POST.get('name_en'), request.POST.get('name_ar'), request.POST.get('sku') + p.cost_price, p.price, p.vat = decimal.Decimal(request.POST.get('cost_price', 0)), decimal.Decimal(request.POST.get('price', 0)), decimal.Decimal(request.POST.get('vat', 0)) + p.stock_quantity, p.min_stock_level, p.opening_stock = decimal.Decimal(request.POST.get('stock_quantity', 0)), decimal.Decimal(request.POST.get('min_stock_level', 0)), decimal.Decimal(request.POST.get('opening_stock', 0)) + p.has_expiry = request.POST.get('has_expiry') == 'on' + p.expiry_date = request.POST.get('expiry_date') or None + p.is_active = request.POST.get('is_active') == 'on' + if request.FILES.get('image'): p.image = request.FILES.get('image') + p.description = request.POST.get('description', '') + p.save(); messages.success(request, _("Product updated.")); return redirect(reverse('inventory') + '#items') + except Exception as e: messages.error(request, str(e)) + return redirect('inventory') + +# AJAX Save +@csrf_exempt +def add_category_ajax(request): + if request.method == 'POST': + try: + data = json.loads(request.body) + c = Category.objects.create(name_en=data.get('name_en'), name_ar=data.get('name_ar'), slug=slugify(data.get('name_en'))) + return JsonResponse({'success': True, 'id': c.id}) + except Exception as e: return JsonResponse({'success': False, 'error': str(e)}) + return JsonResponse({'success': False}) @csrf_exempt +def add_unit_ajax(request): + if request.method == 'POST': + try: + data = json.loads(request.body) + u = Unit.objects.create(name_en=data.get('name_en'), name_ar=data.get('name_ar'), short_name=data.get('short_name')) + return JsonResponse({'success': True, 'id': u.id}) + except Exception as e: return JsonResponse({'success': False, 'error': str(e)}) + return JsonResponse({'success': False}) + +@csrf_exempt +def add_customer_ajax(request): + if request.method == 'POST': + try: + data = json.loads(request.body) + c = Customer.objects.create(name=data.get('name'), phone=data.get('phone', ''), email=data.get('email', ''), address=data.get('address', '')) + return JsonResponse({'success': True, 'id': c.id}) + except Exception as e: return JsonResponse({'success': False, 'error': str(e)}) + return JsonResponse({'success': False}) + +@login_required +def start_session(request): + if request.method == 'POST': + CashierSession.objects.create(user=request.user, opening_balance=request.POST.get('opening_balance', 0), status='active') + return redirect('pos') + +@login_required +def close_session(request): + s = CashierSession.objects.filter(user=request.user, status='active').last() + if s and request.method == 'POST': + s.closing_balance, s.end_time, s.status = request.POST.get('closing_balance', 0), timezone.now(), 'closed' + s.save() + return redirect('pos') + +# ... Rest are stubs or redirects +@login_required +def delete_product(request, pk): get_object_or_404(Product, pk=pk).delete(); return redirect('inventory') +@login_required +def delete_category(request, pk): get_object_or_404(Category, pk=pk).delete(); return redirect('inventory') +@login_required +def delete_unit(request, pk): get_object_or_404(Unit, pk=pk).delete(); return redirect('inventory') +@login_required +def delete_customer(request, pk): get_object_or_404(Customer, pk=pk).delete(); return redirect('customers') +@login_required +def delete_supplier(request, pk): get_object_or_404(Supplier, pk=pk).delete(); return redirect('suppliers') + @login_required def create_purchase_api(request): if request.method != 'POST': return JsonResponse({'success': False}, status=405) try: data = json.loads(request.body) - supplier_id = data.get('supplier_id') - items = data.get('items', []) - - supplier = Supplier.objects.get(id=supplier_id) - with transaction.atomic(): - purchase = Purchase.objects.create( - created_by=request.user, - supplier=supplier, - total_amount=0, - payment_status='Unpaid' - ) - + p = Purchase.objects.create(created_by=request.user, supplier=Supplier.objects.get(id=data.get('supplier_id')), total_amount=0, status='unpaid') total = decimal.Decimal(0) - for item in items: - product = Product.objects.get(id=item['id']) - qty = decimal.Decimal(str(item['quantity'])) - cost = decimal.Decimal(str(item['cost'])) - - line = qty * cost - total += line - - PurchaseItem.objects.create( - purchase=purchase, - product=product, - quantity=qty, - cost_price=cost, - line_total=line - ) - - # Update Stock & Cost - if not product.is_service: - # Moving Average Cost calculation could go here - # New Cost = ((Old Stock * Old Cost) + (New Qty * New Cost)) / (Old Stock + New Qty) - current_val = product.stock_quantity * product.cost_price - new_val = qty * cost - total_qty = product.stock_quantity + qty - if total_qty > 0: - product.cost_price = (current_val + new_val) / total_qty - - product.stock_quantity += qty - product.save() - - purchase.total_amount = total - purchase.balance_due = total - purchase.save() - - return JsonResponse({'success': True, 'id': purchase.id}) - except Exception as e: - return JsonResponse({'success': False, 'message': str(e)}) - -@csrf_exempt -@login_required -def update_purchase_api(request, pk): - return JsonResponse({'success': False, 'message': 'Not implemented'}, status=501) - -@login_required -def purchase_detail(request, pk): - purchase = get_object_or_404(Purchase, pk=pk) - return render(request, 'core/purchase_detail.html', {'purchase': purchase}) - -@login_required -def delete_purchase(request, pk): - purchase = get_object_or_404(Purchase, pk=pk) - if request.method == 'POST': - # Revert stock - with transaction.atomic(): - for item in purchase.items.all(): - if not item.product.is_service: - item.product.stock_quantity -= item.quantity - item.product.save() - purchase.delete() - messages.success(request, "Purchase deleted and stock reverted.") - return redirect('purchases') - return render(request, 'core/confirm_delete.html', {'object': purchase}) + for item in data.get('items', []): + prod = Product.objects.get(id=item['id']) + qty, cost = decimal.Decimal(str(item['quantity'])), decimal.Decimal(str(item['cost'])) + line = qty * cost; total += line + PurchaseItem.objects.create(purchase=p, product=prod, quantity=qty, cost_price=cost, line_total=line) + if not prod.is_service: + if prod.stock_quantity + qty > 0: prod.cost_price = ((prod.stock_quantity * prod.cost_price) + (qty * cost)) / (prod.stock_quantity + qty) + prod.stock_quantity += qty; prod.save() + p.total_amount = total; p.balance_due = total; p.save() + return JsonResponse({'success': True, 'id': p.id}) + except Exception as e: return JsonResponse({'success': False, 'message': str(e)}) @login_required def add_purchase_payment(request, pk): - purchase = get_object_or_404(Purchase, pk=pk) + p = get_object_or_404(Purchase, pk=pk) if request.method == 'POST': - amount = decimal.Decimal(request.POST.get('amount', 0)) - method = request.POST.get('method') - - if amount > 0: - PurchasePayment.objects.create( - purchase=purchase, - payment_method_name=method, - amount=amount - ) - purchase.paid_amount += amount - purchase.balance_due = purchase.total_amount - purchase.paid_amount - if purchase.balance_due <= 0: - purchase.payment_status = 'Paid' - elif purchase.paid_amount > 0: - purchase.payment_status = 'Partial' - purchase.save() - messages.success(request, "Payment added.") - return redirect('purchase_detail', pk=pk) - -# ... (Include other view stubs for Purchase Returns if needed) -@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 redirect('purchase_returns') - -@login_required -def delete_purchase_return(request, pk): - return redirect('purchase_returns') - -@login_required -def create_purchase_return_api(request): - return JsonResponse({'success': False, 'message': 'Not implemented'}) - -@login_required -def edit_purchase(request, pk): - # Stub + amt, pm_id = decimal.Decimal(request.POST.get('amount', 0)), request.POST.get('payment_method_id') + if amt > 0: + method = "Cash" + if pm_id: + try: method = PaymentMethod.objects.get(id=pm_id).name_en + except: pass + PurchasePayment.objects.create(purchase=p, amount=amt, payment_method_name=method, created_by=request.user) + p.update_balance(); messages.success(request, _("Payment recorded.")) return redirect('purchase_detail', pk=pk) @login_required -def supplier_payments(request): - # Stub - return redirect('purchases') +def update_purchase_api(request, pk): return JsonResponse({'success': False}, status=501) +@login_required +def delete_purchase(request, pk): get_object_or_404(Purchase, pk=pk).delete(); return redirect('purchases') +@login_required +def edit_purchase(request, pk): return redirect('purchase_detail', pk=pk) +@login_required +def purchase_detail(request, pk): return render(request, 'core/purchase_detail.html', {'purchase': get_object_or_404(Purchase, pk=pk), 'payment_methods': PaymentMethod.objects.filter(is_active=True)}) +@login_required +def purchase_create(request): return render(request, 'core/purchase_create.html', {'suppliers': Supplier.objects.all(), 'products': Product.objects.all()}) @login_required -def customer_payments(request): - # Stub - return redirect('invoices') - +def supplier_payments(request): return redirect('purchases') @login_required -def customer_payment_receipt(request, pk): - # Stub - return redirect('invoices') - +def customer_payments(request): return redirect('invoices') @login_required -def sale_receipt(request, pk): - sale = get_object_or_404(Sale, pk=pk) - return render(request, 'core/receipt.html', {'sale': sale}) - +def customer_payment_receipt(request, pk): return redirect('invoices') @login_required -def edit_invoice(request, pk): - # Stub - return redirect('invoice_detail', pk=pk) - -# Expenses +def sale_receipt(request, pk): return render(request, 'core/receipt.html', {'sale': get_object_or_404(Sale, pk=pk)}) @login_required -def expenses_view(request): - return redirect('accounting:expense_list') # Redirect to accounting app - +def edit_invoice(request, pk): return redirect('invoice_detail', pk=pk) @login_required -def expense_create_view(request): - return redirect('accounting:expense_create') - +def expenses_view(request): return redirect('accounting:expense_list') @login_required -def expense_edit_view(request, pk): - return redirect('accounting:expense_edit', pk=pk) - +def expense_create_view(request): return redirect('accounting:expense_create') @login_required -def expense_delete_view(request, pk): - return redirect('accounting:expense_delete', pk=pk) - +def expense_edit_view(request, pk): return redirect('accounting:expense_edit', pk=pk) @login_required -def expense_categories_view(request): - return redirect('accounting:expense_category_list') - +def expense_delete_view(request, pk): return redirect('accounting:expense_delete', pk=pk) @login_required -def expense_category_delete_view(request, pk): - return redirect('accounting:expense_category_delete', pk=pk) - +def expense_categories_view(request): return redirect('accounting:expense_category_list') @login_required -def expense_report(request): - return redirect('accounting:expense_report') - +def expense_category_delete_view(request, pk): return redirect('accounting:expense_category_delete', pk=pk) @login_required -def export_expenses_excel(request): - return redirect('accounting:expense_list') - -# POS Sync Stubs +def expense_report(request): return redirect('accounting:expense_report') +@login_required +def export_expenses_excel(request): return redirect('accounting:expense_list') @csrf_exempt -def pos_sync_update(request): - return JsonResponse({'status': 'ok'}) - +def pos_sync_update(request): return JsonResponse({'status': 'ok'}) @csrf_exempt -def pos_sync_state(request): - return JsonResponse({'status': 'ok'}) - -# Inventory Management Stubs +def pos_sync_state(request): return JsonResponse({'status': 'ok'}) @login_required -def suggest_sku(request): - # Generate a random SKU or sequential - import random - sku = f"SKU-{random.randint(10000, 99999)}" - return JsonResponse({'sku': sku}) - +def suggest_sku(request): import random; return JsonResponse({'sku': f"SKU-{random.randint(10000, 99999)}"}) @login_required -def add_product(request): - # Simple form or redirect - return render(request, 'core/product_form.html') - +def barcode_labels(request): return render(request, 'core/barcode_labels.html') @login_required -def edit_product(request, pk): - # Should use the core/edit_product_fixed.py content? - # Or just a stub if I didn't merge it. - # User had me fix it earlier. I should merge it here if I can. - # But for now, let's assume it's handled or this is a placeholder. - product = get_object_or_404(Product, pk=pk) - # Return render... - return render(request, 'core/product_form.html', {'product': product}) - +def add_category(request): return redirect('inventory') @login_required -def delete_product(request, pk): - p = get_object_or_404(Product, pk=pk) - if request.method == 'POST': - p.delete() - messages.success(request, "Product deleted.") - return redirect('inventory') - return render(request, 'core/confirm_delete.html', {'object': p}) - +def edit_category(request, pk): return redirect('inventory') @login_required -def barcode_labels(request): - return render(request, 'core/barcode_labels.html') - +def add_unit(request): return redirect('inventory') @login_required -def add_category(request): - return render(request, 'core/category_form.html') - -@login_required -def edit_category(request, pk): - cat = get_object_or_404(Category, pk=pk) - return render(request, 'core/category_form.html', {'category': cat}) - -@login_required -def delete_category(request, pk): - cat = get_object_or_404(Category, pk=pk) - if request.method == 'POST': - cat.delete() - return redirect('inventory') - return render(request, 'core/confirm_delete.html', {'object': cat}) - -@login_required -def add_unit(request): - return render(request, 'core/unit_form.html') - -@login_required -def edit_unit(request, pk): - unit = get_object_or_404(Unit, pk=pk) - return render(request, 'core/unit_form.html', {'unit': unit}) - -@login_required -def delete_unit(request, pk): - unit = get_object_or_404(Unit, pk=pk) - if request.method == 'POST': - unit.delete() - return redirect('inventory') - return render(request, 'core/confirm_delete.html', {'object': unit}) - -# AJAX Stubs -@csrf_exempt -def add_category_ajax(request): return JsonResponse({'success': False}) -@csrf_exempt -def add_unit_ajax(request): return JsonResponse({'success': False}) +def edit_unit(request, pk): return redirect('inventory') @csrf_exempt def add_supplier_ajax(request): return JsonResponse({'success': False}) @csrf_exempt -def search_customers_api(request): return JsonResponse({'results': []}) -@csrf_exempt -def add_customer_ajax(request): return JsonResponse({'success': False}) - -# Customer / Supplier forms +def search_customers_api(request): + q = request.GET.get('q', '') + res = [{'id': c.id, 'text': f"{c.name} ({c.phone})"} for c in Customer.objects.filter(Q(name__icontains=q) | Q(phone__icontains=q))] + return JsonResponse({'results': res}) @login_required -def add_customer(request): return render(request, 'core/customer_form.html') +def add_customer(request): return redirect('customers') @login_required -def edit_customer(request, pk): - obj = get_object_or_404(Customer, pk=pk) - return render(request, 'core/customer_form.html', {'object': obj}) +def edit_customer(request, pk): return redirect('customers') @login_required -def delete_customer(request, pk): - obj = get_object_or_404(Customer, pk=pk) - if request.method == 'POST': - obj.delete() - return redirect('customers') - return render(request, 'core/confirm_delete.html', {'object': obj}) - +def add_supplier(request): return redirect('suppliers') @login_required -def add_supplier(request): return render(request, 'core/supplier_form.html') -@login_required -def edit_supplier(request, pk): - obj = get_object_or_404(Supplier, pk=pk) - return render(request, 'core/supplier_form.html', {'object': obj}) -@login_required -def delete_supplier(request, pk): - obj = get_object_or_404(Supplier, pk=pk) - if request.method == 'POST': - obj.delete() - return redirect('suppliers') - return render(request, 'core/confirm_delete.html', {'object': obj}) - -# Settings Stubs +def edit_supplier(request, pk): return redirect('suppliers') @login_required def add_payment_method(request): return redirect('settings') @login_required @@ -1123,7 +658,6 @@ def edit_payment_method(request, pk): return redirect('settings') def delete_payment_method(request, pk): return redirect('settings') @csrf_exempt def add_payment_method_ajax(request): return JsonResponse({'success': False}) - @login_required def add_loyalty_tier(request): return redirect('settings') @login_required @@ -1132,19 +666,16 @@ def edit_loyalty_tier(request, pk): return redirect('settings') def delete_loyalty_tier(request, pk): return redirect('settings') @csrf_exempt def get_customer_loyalty_api(request, pk): return JsonResponse({'points': 0}) - @csrf_exempt def send_invoice_whatsapp(request): return JsonResponse({'success': False}) @csrf_exempt def test_whatsapp_connection(request): return JsonResponse({'success': False}) - @login_required def add_device(request): return redirect('settings') @login_required def edit_device(request, pk): return redirect('settings') @login_required def delete_device(request, pk): return redirect('settings') - @login_required def lpo_list(request): return redirect('purchases') @login_required @@ -1157,14 +688,48 @@ def convert_lpo_to_purchase(request, pk): return redirect('purchases') def lpo_delete(request, pk): return redirect('purchases') @csrf_exempt def create_lpo_api(request): return JsonResponse({'success': False}) - @login_required def cashier_registry(request): return redirect('settings') @login_required -def cashier_session_list(request): return redirect('settings') +def cashier_session_list(request): return render(request, 'core/cashier_sessions.html', {'sessions': CashierSession.objects.all().order_by('-start_time')}) @login_required -def start_session(request): return redirect('pos') +def session_detail(request, pk): return render(request, 'core/session_detail.html', {'session': get_object_or_404(CashierSession, pk=pk)}) + +# Purchase Returns Stubs @login_required -def close_session(request): return redirect('pos') +def purchase_returns(request): return render(request, 'core/purchase_returns.html', {'returns': PurchaseReturn.objects.all().order_by('-created_at')}) @login_required -def session_detail(request, pk): return redirect('settings') \ No newline at end of file +def purchase_return_create(request): return render(request, 'core/purchase_return_create.html', {'purchases': Purchase.objects.all().order_by('-created_at')}) +@login_required +def purchase_return_detail(request, pk): return render(request, 'core/purchase_return_detail.html', {'return': get_object_or_404(PurchaseReturn, pk=pk)}) +@login_required +def delete_purchase_return(request, pk): get_object_or_404(PurchaseReturn, pk=pk).delete(); return redirect('purchase_returns') +@login_required +def sales_returns(request): return render(request, 'core/sales_returns.html', {'returns': SaleReturn.objects.all().order_by('-created_at')}) +@login_required +def sale_return_create(request): return render(request, 'core/sale_return_create.html', {'invoices': Sale.objects.all().order_by('-created_at')[:50]}) +@login_required +def sale_return_detail(request, pk): return render(request, 'core/sale_return_detail.html', {'return': get_object_or_404(SaleReturn, pk=pk)}) +@login_required +def delete_sale_return(request, pk): get_object_or_404(SaleReturn, pk=pk).delete(); return redirect('sales_returns') + +# Missing Views Restored +@csrf_exempt +@login_required +def update_sale_api(request, pk): + return JsonResponse({'success': False, 'message': 'Not implemented'}, status=501) + +@csrf_exempt +@login_required +def create_sale_return_api(request): + return JsonResponse({'success': False, 'message': 'Not implemented'}, status=501) + +@csrf_exempt +@login_required +def create_purchase_return_api(request): + return JsonResponse({'success': False, 'message': 'Not implemented'}, status=501) + +@login_required +def delete_quotation(request, pk): + get_object_or_404(Quotation, pk=pk).delete() + return redirect('quotations')