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.urls import include, path
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static 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 = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
@ -16,3 +25,6 @@ if settings.DEBUG:
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_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 = [ operations = [
migrations.RunSQL( # Modified to handle inconsistent database state (column already missing)
sql="ALTER TABLE core_systemsetting DROP COLUMN IF EXISTS logo_url;", migrations.SeparateDatabaseAndState(
reverse_sql="ALTER TABLE core_systemsetting ADD COLUMN IF NOT EXISTS logo_url varchar(200);",
state_operations=[ state_operations=[
migrations.RemoveField( migrations.RemoveField(
model_name='systemsetting', model_name='systemsetting',
name='logo_url', 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( migrations.AddField(
model_name='systemsetting', model_name='systemsetting',
@ -70,4 +74,4 @@ class Migration(migrations.Migration):
name='currency_symbol', name='currency_symbol',
field=models.CharField(default='OMR', max_length=10, verbose_name='Currency Symbol'), field=models.CharField(default='OMR', max_length=10, verbose_name='Currency Symbol'),
), ),
] ]

View File

@ -20,7 +20,8 @@
<!-- Stats Cards --> <!-- Stats Cards -->
<div class="row g-3 mb-4"> <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="card glass-card border-0 p-3 stat-card h-100">
<div class="d-flex align-items-center"> <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"> <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>
</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="card glass-card border-0 p-3 stat-card h-100">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="stat-icon bg-success bg-opacity-10 rounded-3 p-3 me-3"> <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>
<div> <div>
<h6 class="text-muted small mb-1">{% trans "Total Sales" %}</h6> <h6 class="text-muted small mb-1">{% trans "Total Sales" %}</h6>
@ -46,11 +75,11 @@
</div> </div>
</div> </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="card glass-card border-0 p-3 stat-card h-100">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="stat-icon bg-info bg-opacity-10 rounded-3 p-3 me-3"> <div class="stat-icon bg-warning bg-opacity-10 rounded-3 p-3 me-3">
<i class="bi bi-box-seam fs-4 text-info"></i> <i class="bi bi-box-seam fs-4 text-warning"></i>
</div> </div>
<div> <div>
<h6 class="text-muted small mb-1">{% trans "Total Products" %}</h6> <h6 class="text-muted small mb-1">{% trans "Total Products" %}</h6>
@ -59,11 +88,11 @@
</div> </div>
</div> </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="card glass-card border-0 p-3 stat-card h-100">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="stat-icon bg-warning bg-opacity-10 rounded-3 p-3 me-3"> <div class="stat-icon bg-secondary bg-opacity-10 rounded-3 p-3 me-3">
<i class="bi bi-people fs-4 text-warning"></i> <i class="bi bi-people fs-4 text-secondary"></i>
</div> </div>
<div> <div>
<h6 class="text-muted small mb-1">{% trans "Total Customers" %}</h6> <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_products = Product.objects.count()
total_customers = Customer.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 # 2. Charts Data
today = timezone.now().date() today = timezone.now().date()
# A. Monthly Sales (Current Year) # A. Monthly Sales (Current Year)
current_year = today.year current_year = today.year
monthly_sales = Sale.objects.filter(created_at__year=current_year)\ monthly_sales = (Sale.objects.filter(created_at__year=current_year)
.annotate(month=models.functions.ExtractMonth('created_at'))\ .annotate(month=models.functions.ExtractMonth('created_at'))
.values('month')\ .values('month')
.annotate(total=Sum('total_amount'))\ .annotate(total=Sum('total_amount'))
.order_by('month') .order_by('month'))
monthly_labels = [] monthly_labels = []
monthly_data = [] monthly_data = []
@ -56,11 +60,11 @@ def index(request):
# B. Daily Sales (Last 7 Days) # B. Daily Sales (Last 7 Days)
seven_days_ago = today - timedelta(days=6) seven_days_ago = today - timedelta(days=6)
daily_sales = Sale.objects.filter(created_at__date__gte=seven_days_ago)\ daily_sales = (Sale.objects.filter(created_at__date__gte=seven_days_ago)
.annotate(day=models.functions.ExtractDay('created_at'))\ .annotate(day=models.functions.ExtractDay('created_at'))
.values('created_at__date')\ .values('created_at__date')
.annotate(total=Sum('total_amount'))\ .annotate(total=Sum('total_amount'))
.order_by('created_at__date') .order_by('created_at__date'))
chart_labels = [] chart_labels = []
chart_data = [] chart_data = []
@ -79,25 +83,25 @@ def index(request):
chart_data.append(date_map[date_key]) chart_data.append(date_map[date_key])
# C. Sales by Category # C. Sales by Category
category_sales = SaleItem.objects.values('product__category__name_en')\ category_sales = (SaleItem.objects.values('product__category__name_en')
.annotate(total=Sum('line_total'))\ .annotate(total=Sum('line_total'))
.order_by('-total')[:5] .order_by('-total')[:5])
category_labels = [item['product__category__name_en'] for item in category_sales] category_labels = [item['product__category__name_en'] for item in category_sales]
category_data = [float(item['total']) for item in category_sales] category_data = [float(item['total']) for item in category_sales]
# D. Payment Methods # D. Payment Methods
payment_stats = SalePayment.objects.values('payment_method_name')\ payment_stats = (SalePayment.objects.values('payment_method_name')
.annotate(total=Sum('amount'))\ .annotate(total=Sum('amount'))
.order_by('-total') .order_by('-total'))
payment_labels = [item['payment_method_name'] if item['payment_method_name'] else 'Unknown' for item in payment_stats] 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] payment_data = [float(item['total']) for item in payment_stats]
# 3. Top Products # 3. Top Products
top_products = SaleItem.objects.values('product__name_en', 'product__name_ar')\ top_products = (SaleItem.objects.values('product__name_en', 'product__name_ar')
.annotate(total_qty=Sum('quantity'), total_rev=Sum('line_total'))\ .annotate(total_qty=Sum('quantity'), total_rev=Sum('line_total'))
.order_by('-total_rev')[:5] .order_by('-total_rev')[:5])
# 4. Recent Sales # 4. Recent Sales
recent_sales = Sale.objects.select_related('customer').order_by('-created_at')[:5] 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_sales_count': total_sales_count,
'total_products': total_products, 'total_products': total_products,
'total_customers': total_customers, 'total_customers': total_customers,
'total_receivables': total_receivables,
'total_payables': total_payables,
'monthly_labels': json.dumps(monthly_labels), 'monthly_labels': json.dumps(monthly_labels),
'monthly_data': json.dumps(monthly_data), 'monthly_data': json.dumps(monthly_data),
'chart_labels': json.dumps(chart_labels), '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() session = CashierSession.objects.filter(user=request.user, end_time__isnull=True).last()
# Retrieve held sales # 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 = { context = {
'categories': categories, 'categories': categories,
@ -311,18 +317,43 @@ def create_sale_api(request):
customer_id = data.get('customer_id') customer_id = data.get('customer_id')
items = data.get('items', []) items = data.get('items', [])
payments = data.get('payments', []) 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))) discount = decimal.Decimal(str(data.get('discount', 0)))
notes = data.get('notes', '') notes = data.get('notes', '')
invoice_number = data.get('invoice_number', '')
due_date = data.get('due_date')
if not items: if not items:
return JsonResponse({'success': False, 'message': 'No items in cart'}, status=400) return JsonResponse({'success': False, 'message': 'No items in cart'}, status=400)
# Validate Session # Validate Session
session = CashierSession.objects.filter(user=request.user, end_time__isnull=True).last() session = CashierSession.objects.filter(user=request.user, end_time__isnull=True).last()
if not session: # if not session: ... (Optional check)
# Allow admin to sell without session? Or enforce? Let's enforce for now but check logic.
# Assuming logic enforces session.
pass
with transaction.atomic(): with transaction.atomic():
customer = None customer = None
@ -330,36 +361,50 @@ def create_sale_api(request):
customer = Customer.objects.get(id=customer_id) customer = Customer.objects.get(id=customer_id)
sale = Sale.objects.create( sale = Sale.objects.create(
user=request.user, created_by=request.user,
customer=customer, customer=customer,
invoice_number=invoice_number,
total_amount=0, # Will calculate total_amount=0, # Will calculate
discount=discount, discount=discount,
notes=notes, notes=notes,
payment_status='Pending' payment_status='Pending'
) )
if due_date:
sale.due_date = due_date
subtotal = decimal.Decimal(0) subtotal = decimal.Decimal(0)
vat_amount = decimal.Decimal(0) # Track total VAT
for item in items: for item in items:
product = Product.objects.select_for_update().get(id=item['id']) product = Product.objects.select_for_update().get(id=item['id'])
qty = decimal.Decimal(str(item['quantity'])) 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 # Check System Setting for Zero Stock
if not product.is_service and product.stock_quantity < qty: setting = SystemSetting.objects.first()
# Check system setting for allow zero stock allow_zero = setting.allow_zero_stock_sales if setting else False
setting = SystemSetting.objects.first()
if not setting or not setting.allow_zero_stock_sales: if not product.is_service and product.stock_quantity < qty and not allow_zero:
raise Exception(f"Insufficient stock for {product.name_en}") raise Exception(f"Insufficient stock for {product.name_en}")
line_total = price * qty line_total = price * qty
subtotal += line_total 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( SaleItem.objects.create(
sale=sale, sale=sale,
product=product, product=product,
quantity=qty, quantity=qty,
price=price, unit_price=price, # Fixed field name
line_total=line_total line_total=line_total
) )
@ -368,9 +413,10 @@ def create_sale_api(request):
product.stock_quantity -= qty product.stock_quantity -= qty
product.save() product.save()
total_amount = subtotal - discount # Recalculate Totals
sale.subtotal = subtotal sale.subtotal = subtotal
sale.total_amount = total_amount sale.vat_amount = vat_amount
sale.total_amount = subtotal + vat_amount - discount
# Process Payments # Process Payments
paid_amount = decimal.Decimal(0) paid_amount = decimal.Decimal(0)
@ -381,12 +427,13 @@ def create_sale_api(request):
SalePayment.objects.create( SalePayment.objects.create(
sale=sale, sale=sale,
payment_method_name=method_name, payment_method_name=method_name,
amount=amount amount=amount,
created_by=request.user
) )
paid_amount += amount paid_amount += amount
sale.paid_amount = paid_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: if sale.balance_due <= 0:
sale.payment_status = 'Paid' 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'}) return JsonResponse({'success': True, 'sale_id': sale.id, 'message': 'Sale created successfully'})
except Exception as e: except Exception as e:
import traceback
traceback.print_exc()
return JsonResponse({'success': False, 'message': str(e)}, status=500) return JsonResponse({'success': False, 'message': str(e)}, status=500)
@csrf_exempt @csrf_exempt
@ -419,7 +468,7 @@ def hold_sale_api(request):
customer_name = data.get('customer_name', '') customer_name = data.get('customer_name', '')
HeldSale.objects.create( HeldSale.objects.create(
user=request.user, created_by=request.user,
cart_data=cart_data, cart_data=cart_data,
note=note, note=note,
customer_name=customer_name customer_name=customer_name
@ -430,7 +479,7 @@ def hold_sale_api(request):
@login_required @login_required
def get_held_sales_api(request): 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 = [] data = []
for s in sales: for s in sales:
data.append({ data.append({
@ -447,7 +496,7 @@ def get_held_sales_api(request):
def recall_held_sale_api(request, pk): def recall_held_sale_api(request, pk):
# Just return the data, maybe delete it or keep it until finalized? # 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. # 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({ return JsonResponse({
'success': True, 'success': True,
'cart_data': json.loads(held_sale.cart_data), 'cart_data': json.loads(held_sale.cart_data),
@ -458,7 +507,7 @@ def recall_held_sale_api(request, pk):
@csrf_exempt @csrf_exempt
@login_required @login_required
def delete_held_sale_api(request, pk): 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() held_sale.delete()
return JsonResponse({'success': True}) return JsonResponse({'success': True})
@ -468,7 +517,7 @@ def delete_held_sale_api(request, pk):
@login_required @login_required
def invoice_list(request): 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 # Filter
status = request.GET.get('status') status = request.GET.get('status')
@ -481,12 +530,21 @@ def invoice_list(request):
sales = sales.filter(created_at__date__gte=start_date) sales = sales.filter(created_at__date__gte=start_date)
if end_date: if end_date:
sales = sales.filter(created_at__date__lte=end_date) sales = sales.filter(created_at__date__lte=end_date)
customers = Customer.objects.all()
paginator = Paginator(sales, 20) paginator = Paginator(sales, 20)
page_number = request.GET.get('page') page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number) 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 @login_required
def invoice_detail(request, pk): def invoice_detail(request, pk):
@ -495,10 +553,20 @@ def invoice_detail(request, pk):
@login_required @login_required
def invoice_create(request): def invoice_create(request):
# Reuse POS or a specific invoice form? # Retrieve data for the invoice form
# For now redirect to POS or show a simple form products = Product.objects.filter(is_active=True).select_related('category', 'unit')
# Let's show a simple form page if it exists, else POS customers = Customer.objects.all()
return redirect('pos') # Simplified for now 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 @login_required
def delete_sale(request, pk): def delete_sale(request, pk):
@ -571,7 +639,7 @@ def create_quotation_api(request):
customer = Customer.objects.get(id=customer_id) customer = Customer.objects.get(id=customer_id)
quotation = Quotation.objects.create( quotation = Quotation.objects.create(
user=request.user, created_by=request.user,
customer=customer, customer=customer,
total_amount=0 total_amount=0
) )
@ -616,7 +684,7 @@ def convert_quotation_to_invoice(request, pk):
try: try:
with transaction.atomic(): with transaction.atomic():
sale = Sale.objects.create( sale = Sale.objects.create(
user=request.user, created_by=request.user,
customer=quot.customer, customer=quot.customer,
total_amount=quot.total_amount, total_amount=quot.total_amount,
payment_status='Unpaid', payment_status='Unpaid',
@ -750,7 +818,7 @@ def create_purchase_api(request):
with transaction.atomic(): with transaction.atomic():
purchase = Purchase.objects.create( purchase = Purchase.objects.create(
user=request.user, created_by=request.user,
supplier=supplier, supplier=supplier,
total_amount=0, total_amount=0,
payment_status='Unpaid' payment_status='Unpaid'
@ -1099,4 +1167,4 @@ def start_session(request): return redirect('pos')
@login_required @login_required
def close_session(request): return redirect('pos') def close_session(request): return redirect('pos')
@login_required @login_required
def session_detail(request, pk): return redirect('settings') def session_detail(request, pk): return redirect('settings')

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