adding customer due to dash

This commit is contained in:
Flatlogic Bot 2026-02-09 12:28:28 +00:00
parent db1a6f5278
commit 9299fde7e7
8 changed files with 233 additions and 66 deletions

View File

@ -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"<h1>Debug 404 Catcher</h1><p>You requested: <strong>{request.path}</strong></p><p>This URL was not matched by any standard pattern.</p>")
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('<path:resource>', debug_catcher))

View File

@ -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',

View File

@ -20,7 +20,8 @@
<!-- Stats Cards -->
<div class="row g-3 mb-4">
<div class="col-md-3">
<!-- Row 1: Financials -->
<div class="col-md-4">
<div class="card glass-card border-0 p-3 stat-card h-100">
<div class="d-flex align-items-center">
<div class="stat-icon bg-primary text-white bg-opacity-10 text-primary rounded-3 p-3 me-3">
@ -33,11 +34,39 @@
</div>
</div>
</div>
<div class="col-md-3">
<div class="col-md-4">
<div class="card glass-card border-0 p-3 stat-card h-100">
<div class="d-flex align-items-center">
<div class="stat-icon bg-success bg-opacity-10 rounded-3 p-3 me-3">
<i class="bi bi-cart-check fs-4 text-success"></i>
<i class="bi bi-arrow-down-left-circle fs-4 text-success"></i>
</div>
<div>
<h6 class="text-muted small mb-1">{% trans "Customer Due" %}</h6>
<h4 class="fw-bold mb-0">{{ site_settings.currency_symbol }}{{ total_receivables|floatformat:3 }}</h4>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card glass-card border-0 p-3 stat-card h-100">
<div class="d-flex align-items-center">
<div class="stat-icon bg-danger bg-opacity-10 rounded-3 p-3 me-3">
<i class="bi bi-arrow-up-right-circle fs-4 text-danger"></i>
</div>
<div>
<h6 class="text-muted small mb-1">{% trans "Supplier Due" %}</h6>
<h4 class="fw-bold mb-0">{{ site_settings.currency_symbol }}{{ total_payables|floatformat:3 }}</h4>
</div>
</div>
</div>
</div>
<!-- Row 2: Counts -->
<div class="col-md-4">
<div class="card glass-card border-0 p-3 stat-card h-100">
<div class="d-flex align-items-center">
<div class="stat-icon bg-info bg-opacity-10 rounded-3 p-3 me-3">
<i class="bi bi-cart-check fs-4 text-info"></i>
</div>
<div>
<h6 class="text-muted small mb-1">{% trans "Total Sales" %}</h6>
@ -46,11 +75,11 @@
</div>
</div>
</div>
<div class="col-md-3">
<div class="col-md-4">
<div class="card glass-card border-0 p-3 stat-card h-100">
<div class="d-flex align-items-center">
<div class="stat-icon bg-info bg-opacity-10 rounded-3 p-3 me-3">
<i class="bi bi-box-seam fs-4 text-info"></i>
<div class="stat-icon bg-warning bg-opacity-10 rounded-3 p-3 me-3">
<i class="bi bi-box-seam fs-4 text-warning"></i>
</div>
<div>
<h6 class="text-muted small mb-1">{% trans "Total Products" %}</h6>
@ -59,11 +88,11 @@
</div>
</div>
</div>
<div class="col-md-3">
<div class="col-md-4">
<div class="card glass-card border-0 p-3 stat-card h-100">
<div class="d-flex align-items-center">
<div class="stat-icon bg-warning bg-opacity-10 rounded-3 p-3 me-3">
<i class="bi bi-people fs-4 text-warning"></i>
<div class="stat-icon bg-secondary bg-opacity-10 rounded-3 p-3 me-3">
<i class="bi bi-people fs-4 text-secondary"></i>
</div>
<div>
<h6 class="text-muted small mb-1">{% trans "Total Customers" %}</h6>

View File

@ -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')
@ -482,11 +531,20 @@ def invoice_list(request):
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'

54
debug_request.py Normal file
View File

@ -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()