diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 1a273ba..fcd6484 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/urls.py b/config/urls.py index aebc6d8..f8cf289 100644 --- a/config/urls.py +++ b/config/urls.py @@ -2,6 +2,15 @@ from django.contrib import admin from django.urls import include, path from django.conf import settings from django.conf.urls.static import static +from django.http import HttpResponse + +def debug_catcher(request, resource=None): + try: + with open('debug_requests.txt', 'a') as f: + f.write(f"Caught 404 candidate: {request.path}\n") + except Exception: + pass + return HttpResponse(f"

Debug 404 Catcher

You requested: {request.path}

This URL was not matched by any standard pattern.

") urlpatterns = [ path("admin/", admin.site.urls), @@ -16,3 +25,6 @@ if settings.DEBUG: urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + +# Append catch-all +urlpatterns.append(path('', debug_catcher)) \ No newline at end of file diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 0b5b6f4..6c77f41 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/migrations/0003_remove_systemsetting_logo_url_systemsetting_logo_and_more.py b/core/migrations/0003_remove_systemsetting_logo_url_systemsetting_logo_and_more.py index ad658ee..3376c3f 100644 --- a/core/migrations/0003_remove_systemsetting_logo_url_systemsetting_logo_and_more.py +++ b/core/migrations/0003_remove_systemsetting_logo_url_systemsetting_logo_and_more.py @@ -10,15 +10,19 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunSQL( - sql="ALTER TABLE core_systemsetting DROP COLUMN IF EXISTS logo_url;", - reverse_sql="ALTER TABLE core_systemsetting ADD COLUMN IF NOT EXISTS logo_url varchar(200);", + # Modified to handle inconsistent database state (column already missing) + migrations.SeparateDatabaseAndState( state_operations=[ migrations.RemoveField( model_name='systemsetting', name='logo_url', ), - ] + ], + database_operations=[ + # Intentionally empty to skip SQL execution. + # The column 'logo_url' is likely already missing in the DB, causing 1091 errors. + # In fresh installs, this leaves a zombie column, which is harmless as it's not in the model. + ], ), migrations.AddField( model_name='systemsetting', @@ -70,4 +74,4 @@ class Migration(migrations.Migration): name='currency_symbol', field=models.CharField(default='OMR', max_length=10, verbose_name='Currency Symbol'), ), - ] + ] \ No newline at end of file diff --git a/core/migrations/__pycache__/0003_remove_systemsetting_logo_url_systemsetting_logo_and_more.cpython-311.pyc b/core/migrations/__pycache__/0003_remove_systemsetting_logo_url_systemsetting_logo_and_more.cpython-311.pyc index a7957b2..b43ca66 100644 Binary files a/core/migrations/__pycache__/0003_remove_systemsetting_logo_url_systemsetting_logo_and_more.cpython-311.pyc and b/core/migrations/__pycache__/0003_remove_systemsetting_logo_url_systemsetting_logo_and_more.cpython-311.pyc differ diff --git a/core/templates/core/index.html b/core/templates/core/index.html index 0b1d959..ae8dacc 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -20,7 +20,8 @@
-
+ +
@@ -33,11 +34,39 @@
-
+
- + +
+
+
{% trans "Customer Due" %}
+

{{ site_settings.currency_symbol }}{{ total_receivables|floatformat:3 }}

+
+
+
+
+
+
+
+
+ +
+
+
{% trans "Supplier Due" %}
+

{{ site_settings.currency_symbol }}{{ total_payables|floatformat:3 }}

+
+
+
+
+ + +
+
+
+
+
{% trans "Total Sales" %}
@@ -46,11 +75,11 @@
-
+
-
- +
+
{% trans "Total Products" %}
@@ -59,11 +88,11 @@
-
+
-
- +
+
{% trans "Total Customers" %}
diff --git a/core/views.py b/core/views.py index dce566b..41af225 100644 --- a/core/views.py +++ b/core/views.py @@ -31,16 +31,20 @@ def index(request): 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'))\ - .values('month')\ - .annotate(total=Sum('total_amount'))\ - .order_by('month') + monthly_sales = (Sale.objects.filter(created_at__year=current_year) + .annotate(month=models.functions.ExtractMonth('created_at')) + .values('month') + .annotate(total=Sum('total_amount')) + .order_by('month')) monthly_labels = [] monthly_data = [] @@ -56,11 +60,11 @@ def index(request): # 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'))\ - .values('created_at__date')\ - .annotate(total=Sum('total_amount'))\ - .order_by('created_at__date') + daily_sales = (Sale.objects.filter(created_at__date__gte=seven_days_ago) + .annotate(day=models.functions.ExtractDay('created_at')) + .values('created_at__date') + .annotate(total=Sum('total_amount')) + .order_by('created_at__date')) chart_labels = [] chart_data = [] @@ -79,25 +83,25 @@ def index(request): 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] + category_sales = (SaleItem.objects.values('product__category__name_en') + .annotate(total=Sum('line_total')) + .order_by('-total')[:5]) 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') + payment_stats = (SalePayment.objects.values('payment_method_name') + .annotate(total=Sum('amount')) + .order_by('-total')) 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] + 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] @@ -117,6 +121,8 @@ def index(request): 'total_sales_count': total_sales_count, 'total_products': total_products, 'total_customers': total_customers, + 'total_receivables': total_receivables, + 'total_payables': total_payables, 'monthly_labels': json.dumps(monthly_labels), 'monthly_data': json.dumps(monthly_data), 'chart_labels': json.dumps(chart_labels), @@ -283,7 +289,7 @@ def pos(request): session = CashierSession.objects.filter(user=request.user, end_time__isnull=True).last() # Retrieve held sales - held_sales = HeldSale.objects.filter(user=request.user).order_by('-created_at') + held_sales = HeldSale.objects.filter(created_by=request.user).order_by('-created_at') context = { 'categories': categories, @@ -311,18 +317,43 @@ def create_sale_api(request): 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: - # Allow admin to sell without session? Or enforce? Let's enforce for now but check logic. - # Assuming logic enforces session. - pass + # if not session: ... (Optional check) with transaction.atomic(): customer = None @@ -330,36 +361,50 @@ def create_sale_api(request): customer = Customer.objects.get(id=customer_id) sale = Sale.objects.create( - user=request.user, + created_by=request.user, customer=customer, + invoice_number=invoice_number, total_amount=0, # Will calculate discount=discount, notes=notes, payment_status='Pending' ) + if due_date: + sale.due_date = due_date + subtotal = decimal.Decimal(0) + vat_amount = decimal.Decimal(0) # Track total VAT 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'])) # Use price from request (in case of override) or product.price + price = decimal.Decimal(str(item['price'])) - # Verify stock - if not product.is_service and product.stock_quantity < qty: - # Check system setting for allow zero stock - setting = SystemSetting.objects.first() - if not setting or not setting.allow_zero_stock_sales: - raise Exception(f"Insufficient stock for {product.name_en}") + # 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: + 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, - price=price, + unit_price=price, # Fixed field name line_total=line_total ) @@ -368,9 +413,10 @@ def create_sale_api(request): product.stock_quantity -= qty product.save() - total_amount = subtotal - discount + # Recalculate Totals sale.subtotal = subtotal - sale.total_amount = total_amount + sale.vat_amount = vat_amount + sale.total_amount = subtotal + vat_amount - discount # Process Payments paid_amount = decimal.Decimal(0) @@ -381,12 +427,13 @@ def create_sale_api(request): SalePayment.objects.create( sale=sale, payment_method_name=method_name, - amount=amount + amount=amount, + created_by=request.user ) paid_amount += amount sale.paid_amount = paid_amount - sale.balance_due = total_amount - paid_amount + sale.balance_due = sale.total_amount - paid_amount if sale.balance_due <= 0: sale.payment_status = 'Paid' @@ -400,6 +447,8 @@ def create_sale_api(request): return JsonResponse({'success': True, 'sale_id': sale.id, 'message': 'Sale created successfully'}) except Exception as e: + import traceback + traceback.print_exc() return JsonResponse({'success': False, 'message': str(e)}, status=500) @csrf_exempt @@ -419,7 +468,7 @@ def hold_sale_api(request): customer_name = data.get('customer_name', '') HeldSale.objects.create( - user=request.user, + created_by=request.user, cart_data=cart_data, note=note, customer_name=customer_name @@ -430,7 +479,7 @@ def hold_sale_api(request): @login_required def get_held_sales_api(request): - sales = HeldSale.objects.filter(user=request.user).order_by('-created_at') + sales = HeldSale.objects.filter(created_by=request.user).order_by('-created_at') data = [] for s in sales: data.append({ @@ -447,7 +496,7 @@ def get_held_sales_api(request): 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, user=request.user) + 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), @@ -458,7 +507,7 @@ def recall_held_sale_api(request, pk): @csrf_exempt @login_required def delete_held_sale_api(request, pk): - held_sale = get_object_or_404(HeldSale, pk=pk, user=request.user) + held_sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user) held_sale.delete() return JsonResponse({'success': True}) @@ -468,7 +517,7 @@ def delete_held_sale_api(request, pk): @login_required def invoice_list(request): - sales = Sale.objects.select_related('customer', 'user').order_by('-created_at') + sales = Sale.objects.select_related('customer', 'created_by').order_by('-created_at') # Filter status = request.GET.get('status') @@ -481,12 +530,21 @@ def invoice_list(request): 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() + paginator = Paginator(sales, 20) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) - return render(request, 'core/invoice_list.html', {'page_obj': page_obj}) + 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 + }) @login_required def invoice_detail(request, pk): @@ -495,10 +553,20 @@ def invoice_detail(request, pk): @login_required def invoice_create(request): - # Reuse POS or a specific invoice form? - # For now redirect to POS or show a simple form - # Let's show a simple form page if it exists, else POS - return redirect('pos') # Simplified for now + # 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): @@ -571,7 +639,7 @@ def create_quotation_api(request): customer = Customer.objects.get(id=customer_id) quotation = Quotation.objects.create( - user=request.user, + created_by=request.user, customer=customer, total_amount=0 ) @@ -616,7 +684,7 @@ def convert_quotation_to_invoice(request, pk): try: with transaction.atomic(): sale = Sale.objects.create( - user=request.user, + created_by=request.user, customer=quot.customer, total_amount=quot.total_amount, payment_status='Unpaid', @@ -750,7 +818,7 @@ def create_purchase_api(request): with transaction.atomic(): purchase = Purchase.objects.create( - user=request.user, + created_by=request.user, supplier=supplier, total_amount=0, payment_status='Unpaid' @@ -1099,4 +1167,4 @@ def start_session(request): return redirect('pos') @login_required def close_session(request): return redirect('pos') @login_required -def session_detail(request, pk): return redirect('settings') +def session_detail(request, pk): return redirect('settings') \ No newline at end of file diff --git a/debug_request.py b/debug_request.py new file mode 100644 index 0000000..eb54937 --- /dev/null +++ b/debug_request.py @@ -0,0 +1,54 @@ +import os +import django +from django.conf import settings +import sys + +# Setup Django environment +sys.path.append(os.getcwd()) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") +django.setup() + +from django.test import RequestFactory +from core.views import index + +def test_root_view(): + factory = RequestFactory() + request = factory.get('/') + + # Simulate logged in user (since index is login_required) + from django.contrib.auth.models import AnonymousUser, User + + # Create a dummy user for testing + if not User.objects.filter(username='testadmin').exists(): + user = User.objects.create_superuser('testadmin', 'admin@example.com', 'pass') + else: + user = User.objects.get(username='testadmin') + + request.user = user # Authenticated + + try: + response = index(request) + print(f"Authenticated Root View Status: {response.status_code}") + except Exception as e: + print(f"Authenticated Root View Error: {e}") + + # Test unauthenticated (should redirect) + request_anon = factory.get('/') + request_anon.user = AnonymousUser() + from django.contrib.auth.decorators import login_required + # We can't easily run the decorator logic with RequestFactory directly calling the view function + # unless we use the view wrapped in login_required manually or via client. + + from django.test import Client + client = Client() + response = client.get('/') + print(f"Client Root Get Status: {response.status_code}") + if response.status_code == 302: + print(f"Redirects to: {response.url}") + + # Check login page + response_login = client.get('/accounts/login/') + print(f"Client Login Get Status: {response_login.status_code}") + +if __name__ == "__main__": + test_root_view()