-
+
+
{% 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()