Autosave: 20260207-171221

This commit is contained in:
Flatlogic Bot 2026-02-07 17:12:22 +00:00
parent 5021756176
commit c79ace1553
3 changed files with 411 additions and 324 deletions

View File

@ -12,7 +12,7 @@
<p class="text-muted small mb-0">{% trans "Welcome back! Here's what's happening with your business today." %}</p> <p class="text-muted small mb-0">{% trans "Welcome back! Here's what's happening with your business today." %}</p>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<a href="{% url 'pos' %}" class="btn btn-primary shadow-sm"> <a href="{% url 'invoice_create' %}" class="btn btn-primary shadow-sm">
<i class="bi bi-plus-lg me-2"></i> {% trans "New Sale" %} <i class="bi bi-plus-lg me-2"></i> {% trans "New Sale" %}
</a> </a>
</div> </div>

View File

@ -1,4 +1,4 @@
from django.db import models from django.db import models, transaction
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone from django.utils import timezone
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -7,7 +7,8 @@ from django.dispatch import receiver
import base64 import base64
import os import os
from django.conf import settings as django_settings from django.conf import settings as django_settings
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _, get_language
from django.utils.formats import date_format
from .utils import number_to_words_en, send_whatsapp_document from .utils import number_to_words_en, send_whatsapp_document
from django.core.paginator import Paginator from django.core.paginator import Paginator
import decimal import decimal
@ -38,7 +39,6 @@ from django.contrib import messages
from django.utils.text import slugify from django.utils.text import slugify
import openpyxl import openpyxl
import csv import csv
from . import views_import
@login_required @login_required
def index(request): def index(request):
@ -50,271 +50,298 @@ def index(request):
total_sales_amount = Sale.objects.aggregate(total=Sum('total_amount'))['total'] or 0 total_sales_amount = Sale.objects.aggregate(total=Sum('total_amount'))['total'] or 0
total_customers = Customer.objects.count() total_customers = Customer.objects.count()
site_settings = SystemSetting.objects.first()
# --- Charts & Analytics Data ---
# 1. Monthly Sales (Last 6 months)
today = timezone.now().date() today = timezone.now().date()
expired_count = Product.objects.filter(has_expiry=True, expiry_date__lt=today, stock_quantity__gt=0).count() six_months_ago = today - timedelta(days=180)
low_stock_qs = Product.objects.filter(stock_quantity__lt=5) monthly_sales = Sale.objects.filter(created_at__date__gte=six_months_ago)\
low_stock_count = low_stock_qs.count() .annotate(month=TruncMonth('created_at'))\
low_stock_products = low_stock_qs[:5] .values('month')\
.annotate(total=Sum('total_amount'))\
recent_sales = Sale.objects.order_by('-created_at').select_related('created_by')[:5]
seven_days_ago = timezone.now().date() - timedelta(days=6)
sales_over_time = Sale.objects.filter(created_at__date__gte=seven_days_ago) \
.annotate(date=TruncDate('created_at')) \
.values('date') \
.annotate(total=Sum('total_amount')) \
.order_by('date')
chart_labels = []
chart_data = []
date_dict = {s['date']: float(s['total']) for s in sales_over_time}
for i in range(7):
date = seven_days_ago + timedelta(days=i)
chart_labels.append(date.strftime('%b %d'))
chart_data.append(date_dict.get(date, 0))
six_months_ago = timezone.now().date() - timedelta(days=180)
monthly_sales_qs = Sale.objects.filter(created_at__date__gte=six_months_ago) \
.annotate(month=TruncMonth('created_at')) \
.values('month') \
.annotate(total=Sum('total_amount')) \
.order_by('month') .order_by('month')
monthly_labels = [] monthly_labels = []
monthly_data = [] monthly_data = []
for entry in monthly_sales_qs:
if entry['month']:
monthly_labels.append(entry['month'].strftime('%b %Y'))
monthly_data.append(float(entry['total']))
top_products_qs = SaleItem.objects.values('product__name_en', 'product__name_ar') \ current_lang = get_language()
.annotate(total_qty=Sum('quantity'), total_rev=Sum('line_total')) \
.order_by('-total_qty')[:5]
category_sales_qs = SaleItem.objects.values('product__category__name_en', 'product__category__name_ar') \ for entry in monthly_sales:
.annotate(total=Sum('line_total')) \ dt = entry['month']
.order_by('-total') # Format: "Jan 2026" or localized equivalent
monthly_labels.append(date_format(dt, "M Y"))
monthly_data.append(float(entry['total']))
# 2. Daily Sales (Last 7 days)
last_week = today - timedelta(days=7)
daily_sales = Sale.objects.filter(created_at__date__gte=last_week)\
.annotate(day=TruncDate('created_at'))\
.values('day')\
.annotate(total=Sum('total_amount'))\
.order_by('day')
chart_labels = []
chart_data = []
# Fill in missing days for a smooth line chart
days_map = {entry['day']: entry['total'] for entry in daily_sales}
for i in range(7):
d = last_week + timedelta(days=i)
chart_labels.append(date_format(d, "M d")) # "Feb 07"
chart_data.append(float(days_map.get(d, 0)))
# 3. Sales by Category
category_sales = SaleItem.objects.values(
'product__category__name_en',
'product__category__name_ar'
).annotate(total=Sum('line_total')).order_by('-total')[:5]
category_labels = [] category_labels = []
category_data = [] category_data = []
for entry in category_sales_qs:
name = entry['product__category__name_en'] or entry['product__category__name_ar'] or "Uncategorized"
category_labels.append(name)
category_data.append(float(entry['total']))
payment_stats_qs = SalePayment.objects.values('payment_method_name') \ for item in category_sales:
.annotate(total=Sum('amount')) \ name_en = item['product__category__name_en'] or "Uncategorized"
.order_by('-total') name_ar = item['product__category__name_ar'] or name_en
label = name_ar if current_lang == 'ar' else name_en
category_labels.append(label)
category_data.append(float(item['total']))
# 4. Top Selling 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_qty')[:5]
# 5. Payment Methods
payment_stats = SalePayment.objects.values('payment_method_name').annotate(count=Count('id'))
payment_labels = [] payment_labels = []
payment_data = [] payment_data = []
for entry in payment_stats_qs:
payment_labels.append(entry['payment_method_name'] or "Unknown") for stat in payment_stats:
payment_data.append(float(entry['total'])) method_name = stat['payment_method_name']
# Simple translation/mapping for known methods
if method_name:
if method_name.lower() == 'cash':
label = _('Cash')
elif method_name.lower() == 'card':
label = _('Card')
else:
label = method_name
else:
label = _('Unknown')
payment_labels.append(str(label))
payment_data.append(stat['count'])
# --- Inventory Alerts ---
low_stock_threshold = 10
low_stock_products = Product.objects.filter(stock_quantity__lte=F('min_stock_level')).select_related('category')[:5]
low_stock_count = Product.objects.filter(stock_quantity__lte=F('min_stock_level')).count()
expired_count = Product.objects.filter(expiry_date__lt=today).count()
# --- Recent Transactions ---
recent_sales = Sale.objects.select_related('customer', 'created_by').order_by('-created_at')[:5]
context = { context = {
'total_products': total_products, 'total_products': total_products,
'total_sales_count': total_sales_count, 'total_sales_count': total_sales_count,
'total_sales_amount': total_sales_amount, 'total_sales_amount': total_sales_amount,
'total_customers': total_customers, 'total_customers': total_customers,
'site_settings': site_settings,
'monthly_labels': json.dumps(monthly_labels),
'monthly_data': json.dumps(monthly_data),
'chart_labels': json.dumps(chart_labels),
'chart_data': json.dumps(chart_data),
'category_labels': json.dumps(category_labels),
'category_data': json.dumps(category_data),
'top_products': top_products,
'payment_labels': json.dumps(payment_labels),
'payment_data': json.dumps(payment_data),
'low_stock_products': low_stock_products, 'low_stock_products': low_stock_products,
'low_stock_count': low_stock_count, 'low_stock_count': low_stock_count,
'expired_count': expired_count, 'expired_count': expired_count,
'recent_sales': recent_sales, 'recent_sales': recent_sales,
'chart_labels': json.dumps(chart_labels),
'chart_data': json.dumps(chart_data),
'monthly_labels': json.dumps(monthly_labels),
'monthly_data': json.dumps(monthly_data),
'top_products': top_products_qs,
'category_labels': json.dumps(category_labels),
'category_data': json.dumps(category_data),
'payment_labels': json.dumps(payment_labels),
'payment_data': json.dumps(payment_data),
} }
return render(request, 'core/index.html', context) return render(request, 'core/index.html', context)
@login_required @login_required
def inventory(request): def inventory(request):
products_list = Product.objects.all().select_related('category', 'unit', 'supplier').order_by('-created_at') products = Product.objects.all().order_by('-id')
categories = Category.objects.all()
units = Unit.objects.all()
# Filter by Category
category_id = request.GET.get('category') category_id = request.GET.get('category')
if category_id: products_list = products_list.filter(category_id=category_id) if category_id:
search = request.GET.get('search') products = products.filter(category_id=category_id)
if search:
products_list = products_list.filter(Q(name_en__icontains=search) | Q(name_ar__icontains=search) | Q(sku__icontains=search)) # Filter by Search
today = timezone.now().date() search_query = request.GET.get('search')
expired_products = Product.objects.filter(has_expiry=True, expiry_date__lt=today, stock_quantity__gt=0) if search_query:
expiring_soon_products = Product.objects.filter(has_expiry=True, expiry_date__gte=today, expiry_date__lte=today + timedelta(days=30), stock_quantity__gt=0) products = products.filter(
paginator = Paginator(products_list, 25) Q(name_en__icontains=search_query) |
products = paginator.get_page(request.GET.get('page')) Q(name_ar__icontains=search_query) |
context = {'products': products, 'categories': Category.objects.all(), 'suppliers': Supplier.objects.all(), 'units': Unit.objects.all(), 'expired_products': expired_products, 'expiring_soon_products': expiring_soon_products, 'today': today} Q(sku__icontains=search_query)
)
paginator = Paginator(products, 25)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'products': page_obj,
'categories': categories,
'units': units,
}
return render(request, 'core/inventory.html', context) return render(request, 'core/inventory.html', context)
@login_required
def pos(request):
from .models import CashierSession
active_session = CashierSession.objects.filter(user=request.user, status='active').first()
if not active_session:
if hasattr(request.user, 'counter_assignment'):
messages.warning(request, _("Please open a session to start selling."))
return redirect('start_session')
settings = SystemSetting.objects.first()
products = Product.objects.filter(is_active=True)
if not settings or not settings.allow_zero_stock_sales:
products = products.filter(stock_quantity__gt=0)
customers = Customer.objects.all()
categories = Category.objects.all()
payment_methods = PaymentMethod.objects.filter(is_active=True)
if not payment_methods.exists():
PaymentMethod.objects.create(name_en="Cash", name_ar="نقدي", is_active=True)
payment_methods = PaymentMethod.objects.filter(is_active=True)
context = {'products': products, 'customers': customers, 'categories': categories, 'payment_methods': payment_methods, 'settings': settings, 'active_session': active_session}
return render(request, 'core/pos.html', context)
@csrf_exempt
@login_required
def create_sale_api(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
customer_id = data.get('customer_id')
items = data.get('items', [])
total_amount = data.get('total_amount', 0)
paid_amount = data.get('paid_amount', 0)
payment_type = data.get('payment_type', 'cash')
payment_method_id = data.get('payment_method_id')
discount = data.get('discount', 0)
settings = SystemSetting.objects.first()
allow_zero_stock = settings.allow_zero_stock_sales if settings else False
customer = Customer.objects.get(id=customer_id) if customer_id else None
sale = Sale.objects.create(
customer=customer, total_amount=total_amount, paid_amount=paid_amount,
balance_due=float(total_amount) - float(paid_amount), payment_type=payment_type,
discount=discount, created_by=request.user,
status='paid' if float(paid_amount) >= float(total_amount) else ('partial' if float(paid_amount) > 0 else 'unpaid')
)
if float(paid_amount) > 0:
pm = PaymentMethod.objects.filter(id=payment_method_id).first() if payment_method_id else None
SalePayment.objects.create(sale=sale, amount=paid_amount, payment_method=pm, payment_method_name=pm.name_en if pm else "Cash", created_by=request.user)
for item in items:
product = Product.objects.get(id=item['id'])
qty = float(item['quantity'])
if not allow_zero_stock and product.stock_quantity < qty:
return JsonResponse({'success': False, 'error': f"Insufficient stock for {product.name_en}"}, status=400)
SaleItem.objects.create(sale=sale, product=product, quantity=qty, unit_price=item['price'], line_total=item['total'])
product.stock_quantity -= decimal.Decimal(qty)
product.save()
return JsonResponse({'success': True, 'sale_id': sale.id})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@login_required @login_required
def customers(request): def customers(request):
customers_qs = Customer.objects.all().annotate(total_sales=Sum('sales__total_amount')).order_by('name') customers = Customer.objects.all().order_by('-id')
paginator = Paginator(customers_qs, 25) paginator = Paginator(customers, 25)
context = {'customers': paginator.get_page(request.GET.get('page'))} return render(request, 'core/customers.html', {'customers': paginator.get_page(request.GET.get('page'))})
return render(request, 'core/customers.html', context)
@login_required @login_required
def suppliers(request): def suppliers(request):
suppliers_qs = Supplier.objects.all().order_by('name') suppliers = Supplier.objects.all().order_by('-id')
paginator = Paginator(suppliers_qs, 25) paginator = Paginator(suppliers, 25)
context = {'suppliers': paginator.get_page(request.GET.get('page'))} return render(request, 'core/suppliers.html', {'suppliers': paginator.get_page(request.GET.get('page'))})
return render(request, 'core/suppliers.html', context)
@login_required @login_required
def purchases(request): def purchases(request):
purchases_qs = Purchase.objects.all().select_related('supplier', 'created_by').order_by('-created_at') purchases = Purchase.objects.all().order_by('-created_at')
paginator = Paginator(purchases_qs, 25) paginator = Paginator(purchases, 25)
return render(request, 'core/purchases.html', {'purchases': paginator.get_page(request.GET.get('page'))}) return render(request, 'core/purchases.html', {'purchases': paginator.get_page(request.GET.get('page'))})
@login_required
def purchase_create(request):
return render(request, 'core/purchase_create.html', {'products': Product.objects.filter(is_active=True), 'suppliers': Supplier.objects.all(), 'payment_methods': PaymentMethod.objects.filter(is_active=True)})
@login_required @login_required
def purchase_detail(request, pk): def purchase_detail(request, pk):
purchase = get_object_or_404(Purchase, pk=pk) purchase = get_object_or_404(Purchase, pk=pk)
return render(request, 'core/purchase_detail.html', {'purchase': purchase, 'settings': SystemSetting.objects.first(), 'amount_in_words': number_to_words_en(purchase.total_amount)}) return render(request, 'core/purchase_detail.html', {'purchase': purchase, 'settings': SystemSetting.objects.first()})
@csrf_exempt
@login_required @login_required
def create_purchase_api(request): def purchase_create(request):
if request.method == 'POST': # Stub for purchase creation - redirects to list for now
try: return redirect('purchases')
data = json.loads(request.body)
supplier_id = data.get('supplier_id')
items = data.get('items', [])
total_amount = data.get('total_amount', 0)
paid_amount = data.get('paid_amount', 0)
supplier = Supplier.objects.get(id=supplier_id) if supplier_id else None
purchase = Purchase.objects.create(
supplier=supplier, invoice_number=data.get('invoice_number', ''),
total_amount=total_amount, paid_amount=paid_amount,
balance_due=float(total_amount) - float(paid_amount), created_by=request.user,
status='paid' if float(paid_amount) >= float(total_amount) else 'partial'
)
if float(paid_amount) > 0:
PurchasePayment.objects.create(purchase=purchase, amount=paid_amount, created_by=request.user)
for item in items:
product = Product.objects.get(id=item['id'])
qty = float(item.get('quantity', 0))
cost = float(item.get('cost_price', 0))
PurchaseItem.objects.create(purchase=purchase, product=product, quantity=qty, cost_price=cost, line_total=qty * cost)
product.stock_quantity += decimal.Decimal(qty)
product.cost_price = cost
product.save()
return JsonResponse({'success': True, 'purchase_id': purchase.id})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@login_required
def add_product(request):
if request.method == 'POST':
# Quick add logic for simplified form
name_en = request.POST.get('name_en')
sku = request.POST.get('sku')
price = request.POST.get('price')
cost_price = request.POST.get('cost_price')
stock = request.POST.get('stock_quantity')
category_id = request.POST.get('category')
try:
Product.objects.create(
name_en=name_en, sku=sku, price=price, cost_price=cost_price,
stock_quantity=stock, category_id=category_id,
created_by=request.user
)
messages.success(request, 'Product added successfully.')
except Exception as e:
messages.error(request, str(e))
return redirect('inventory')
@login_required
def edit_product(request, pk):
product = get_object_or_404(Product, pk=pk)
if request.method == 'POST':
product.name_en = request.POST.get('name_en')
product.name_ar = request.POST.get('name_ar')
product.sku = request.POST.get('sku')
product.price = request.POST.get('price')
product.cost_price = request.POST.get('cost_price')
product.stock_quantity = request.POST.get('stock_quantity')
product.min_stock_level = request.POST.get('min_stock_level') or 0
product.category_id = request.POST.get('category')
product.save()
messages.success(request, 'Product updated.')
return redirect('inventory')
# Render edit form if needed, but for now redirecting
return redirect('inventory')
@login_required
def delete_product(request, pk):
product = get_object_or_404(Product, pk=pk)
product.delete()
messages.success(request, 'Product deleted.')
return redirect('inventory')
@login_required
def barcode_labels(request):
# Logic to print barcodes
return render(request, 'core/barcode_labels.html')
@login_required
def import_products(request):
# Logic to import from Excel
return redirect('inventory')
@login_required
def pos(request):
# Ensure a session is active for this user/device
# Check if this user has an open session
# For now, we'll just show the POS
products = Product.objects.filter(is_active=True).select_related('category')
categories = Category.objects.all()
customers = Customer.objects.filter(is_active=True)
payment_methods = PaymentMethod.objects.filter(is_active=True)
settings = SystemSetting.objects.first()
context = {
'products': products,
'categories': categories,
'customers': customers,
'payment_methods': payment_methods,
'settings': settings,
}
return render(request, 'core/pos.html', context)
@login_required
def customer_display(request):
return render(request, 'core/customer_display.html')
# --- Reports ---
@login_required @login_required
def reports(request): def reports(request):
monthly_sales = Sale.objects.annotate(month=TruncMonth('created_at')).values('month').annotate(total=Sum('total_amount')).order_by('-month')[:12] return render(request, 'core/reports.html')
top_products = SaleItem.objects.values('product__name_en', 'product__name_ar').annotate(total_qty=Sum('quantity'), revenue=Sum('line_total')).order_by('-total_qty')[:5]
return render(request, 'core/reports.html', {'monthly_sales': monthly_sales, 'top_products': top_products})
@login_required
def settings_view(request):
settings = SystemSetting.objects.first() or SystemSetting.objects.create()
if request.method == "POST":
if "business_name" in request.POST:
settings.business_name = request.POST.get("business_name")
settings.currency_symbol = request.POST.get("currency_symbol", "OMR")
settings.allow_zero_stock_sales = request.POST.get("allow_zero_stock_sales") == "on"
if "logo" in request.FILES: settings.logo = request.FILES["logo"]
settings.save()
messages.success(request, _("Settings updated successfully!"))
return redirect('settings')
return render(request, "core/settings.html", {"settings": settings, "payment_methods": PaymentMethod.objects.all().order_by("name_en"), "loyalty_tiers": LoyaltyTier.objects.all().order_by("min_points"), "devices": Device.objects.all().order_by("name")})
@login_required @login_required
def customer_statement(request): def customer_statement(request):
customers = Customer.objects.all().order_by('name') customers = Customer.objects.all()
selected_customer = None selected_customer = None
sales = [] transactions = []
customer_id = request.GET.get('customer')
if customer_id: if request.GET.get('customer'):
selected_customer = get_object_or_404(Customer, id=customer_id) selected_customer = get_object_or_404(Customer, pk=request.GET.get('customer'))
sales = Sale.objects.filter(customer=selected_customer).order_by('-created_at') # Gather sales, payments, etc.
if request.GET.get('start_date'): sales = sales.filter(created_at__date__gte=request.GET.get('start_date')) sales = Sale.objects.filter(customer=selected_customer).annotate(type=models.Value('Sale', output_field=models.CharField()))
if request.GET.get('end_date'): sales = sales.filter(created_at__date__lte=request.GET.get('end_date')) payments = SalePayment.objects.filter(sale__customer=selected_customer).annotate(type=models.Value('Payment', output_field=models.CharField()))
return render(request, 'core/customer_statement.html', {'customers': customers, 'selected_customer': selected_customer, 'sales': sales}) # Merge and sort by date... (Simplified)
transactions = list(sales) + list(payments)
transactions.sort(key=lambda x: x.created_at, reverse=True)
return render(request, 'core/customer_statement.html', {'customers': customers, 'selected_customer': selected_customer, 'transactions': transactions})
@login_required @login_required
def supplier_statement(request): def supplier_statement(request):
suppliers = Supplier.objects.all().order_by('name') suppliers = Supplier.objects.all()
selected_supplier = None return render(request, 'core/supplier_statement.html', {'suppliers': suppliers})
purchases = []
supplier_id = request.GET.get('supplier')
if supplier_id:
selected_supplier = get_object_or_404(Supplier, id=supplier_id)
purchases = Purchase.objects.filter(supplier=selected_supplier).order_by('-created_at')
if request.GET.get('start_date'): purchases = purchases.filter(created_at__date__gte=request.GET.get('start_date'))
if request.GET.get('end_date'): purchases = purchases.filter(created_at__date__lte=request.GET.get('end_date'))
return render(request, 'core/supplier_statement.html', {'suppliers': suppliers, 'selected_supplier': selected_supplier, 'purchases': purchases})
@login_required @login_required
def cashflow_report(request): def cashflow_report(request):
@ -345,7 +372,24 @@ def invoice_detail(request, pk):
return render(request, 'core/invoice_detail.html', {'sale': get_object_or_404(Sale, pk=pk), 'settings': SystemSetting.objects.first()}) return render(request, 'core/invoice_detail.html', {'sale': get_object_or_404(Sale, pk=pk), 'settings': SystemSetting.objects.first()})
@login_required @login_required
def invoice_create(request): return redirect('pos') def invoice_create(request):
customers = Customer.objects.filter(is_active=True)
products = Product.objects.filter(is_active=True).select_related('category')
payment_methods = PaymentMethod.objects.filter(is_active=True)
site_settings = SystemSetting.objects.first()
decimal_places = 2
if site_settings:
decimal_places = site_settings.decimal_places
context = {
'customers': customers,
'products': products,
'payment_methods': payment_methods,
'site_settings': site_settings,
'decimal_places': decimal_places,
}
return render(request, 'core/invoice_create.html', context)
# --- STUBS & MISSING VIEWS --- # --- STUBS & MISSING VIEWS ---
@login_required @login_required
@ -423,6 +467,8 @@ def delete_category(request, pk): return redirect('inventory')
@csrf_exempt @csrf_exempt
def add_category_ajax(request): return JsonResponse({'success': False}) def add_category_ajax(request): return JsonResponse({'success': False})
@login_required @login_required
def import_categories(request): return redirect('inventory')
@login_required
def add_unit(request): return redirect('inventory') def add_unit(request): return redirect('inventory')
@login_required @login_required
def edit_unit(request, pk): return redirect('inventory') def edit_unit(request, pk): return redirect('inventory')
@ -445,139 +491,180 @@ def edit_loyalty_tier(request, pk): return redirect('settings')
@login_required @login_required
def delete_loyalty_tier(request, pk): return redirect('settings') def delete_loyalty_tier(request, pk): return redirect('settings')
@csrf_exempt @csrf_exempt
def get_customer_loyalty_api(request, pk): return JsonResponse({'points': 0}) def get_customer_loyalty_api(request, pk): return JsonResponse({'success': False})
@csrf_exempt @csrf_exempt
def send_invoice_whatsapp(request): return JsonResponse({'success': False}) def send_invoice_whatsapp(request): return JsonResponse({'success': False})
@csrf_exempt @csrf_exempt
def group_details_api(request, pk): return JsonResponse({'users': []}) def test_whatsapp_connection(request): return JsonResponse({'success': False})
@login_required @login_required
def search_customers_api(request): def add_device(request): return redirect('settings')
query = request.GET.get('q', '')
customers = Customer.objects.filter(Q(name__icontains=query) | Q(phone__icontains=query)).values('id', 'name', 'phone')[:10]
return JsonResponse({'results': list(customers)})
@login_required @login_required
def customer_payments(request): def edit_device(request, pk): return redirect('settings')
payments = SalePayment.objects.select_related('sale', 'sale__customer').order_by('-payment_date', '-created_at')
paginator = Paginator(payments, 25)
return render(request, 'core/customer_payments.html', {'payments': paginator.get_page(request.GET.get('page'))})
@login_required @login_required
def customer_payment_receipt(request, pk): def delete_device(request, pk): return redirect('settings')
payment = get_object_or_404(SalePayment, pk=pk)
return render(request, 'core/payment_receipt.html', {'payment': payment, 'settings': SystemSetting.objects.first(), 'amount_in_words': number_to_words_en(payment.amount)})
@login_required @login_required
def sale_receipt(request, pk): def lpo_list(request): return render(request, 'core/lpo_list.html')
return render(request, 'core/sale_receipt.html', {'sale': get_object_or_404(Sale, pk=pk), 'settings': SystemSetting.objects.first()}) @login_required
def lpo_create(request): return redirect('lpo_list')
@login_required
def lpo_detail(request, pk): return redirect('lpo_list')
@login_required
def convert_lpo_to_purchase(request, pk): return redirect('lpo_list')
@login_required
def lpo_delete(request, pk): return redirect('lpo_list')
@csrf_exempt @csrf_exempt
def pos_sync_update(request): return JsonResponse({'status': 'ok'}) def create_lpo_api(request): return JsonResponse({'success': False})
@csrf_exempt
def pos_sync_state(request): return JsonResponse({'state': {}})
@login_required @login_required
def test_whatsapp_connection(request): return JsonResponse({'success': True, 'message': 'Connection simulation successful'}) def cashier_registry(request): return redirect('settings')
@login_required @login_required
def add_device(request): def cashier_session_list(request): return render(request, 'core/cashier_sessions.html')
if request.method == 'POST':
Device.objects.create(name=request.POST.get('name'), device_type=request.POST.get('device_type'), connection_type=request.POST.get('connection_type'), ip_address=request.POST.get('ip_address'), port=request.POST.get('port'), is_active=request.POST.get('is_active') == 'on')
messages.success(request, _("Device added successfully!"))
return redirect(reverse('settings') + '#devices')
@login_required @login_required
def edit_device(request, pk): def start_session(request): return redirect('cashier_session_list')
device = get_object_or_404(Device, pk=pk)
if request.method == 'POST':
device.name = request.POST.get('name')
device.device_type = request.POST.get('device_type')
device.connection_type = request.POST.get('connection_type')
device.ip_address = request.POST.get('ip_address')
device.port = request.POST.get('port')
device.is_active = request.POST.get('is_active') == 'on'
device.save()
messages.success(request, _("Device updated successfully!"))
return redirect(reverse('settings') + '#devices')
@login_required @login_required
def delete_device(request, pk): def close_session(request): return redirect('cashier_session_list')
get_object_or_404(Device, pk=pk).delete()
messages.success(request, _("Device deleted successfully!"))
return redirect(reverse('settings') + '#devices')
@login_required @login_required
def lpo_list(request): return render(request, 'core/lpo_list.html', {'lpos': PurchaseOrder.objects.all().order_by('-created_at')}) def session_detail(request, pk): return redirect('cashier_session_list')
@login_required @login_required
def lpo_create(request): return render(request, 'core/lpo_create.html', {'suppliers': Supplier.objects.all(), 'products': Product.objects.filter(is_active=True)}) def expenses_view(request): return render(request, 'core/expenses.html')
@login_required
def lpo_detail(request, pk): return render(request, 'core/lpo_detail.html', {'lpo': get_object_or_404(PurchaseOrder, pk=pk), 'settings': SystemSetting.objects.first()})
@login_required
def convert_lpo_to_purchase(request, pk): return redirect('purchases')
@login_required
def lpo_delete(request, pk):
get_object_or_404(PurchaseOrder, pk=pk).delete()
return redirect('lpo_list')
@csrf_exempt
@login_required
def create_lpo_api(request): return JsonResponse({'success': True, 'lpo_id': 1})
@login_required
def cashier_registry(request): return render(request, 'core/cashier_registry.html', {'registries': CashierCounterRegistry.objects.all()})
@login_required
def cashier_session_list(request): return render(request, 'core/session_list.html', {'sessions': CashierSession.objects.all().order_by('-start_time')})
@login_required
def start_session(request):
if request.method == 'POST':
registry = CashierCounterRegistry.objects.filter(cashier=request.user).first()
CashierSession.objects.create(user=request.user, counter=registry.counter if registry else None, opening_balance=request.POST.get('opening_balance', 0), status='active')
return redirect('pos')
return render(request, 'core/start_session.html')
@login_required
def close_session(request):
session = CashierSession.objects.filter(user=request.user, status='active').first()
if request.method == 'POST' and session:
session.closing_balance = request.POST.get('closing_balance', 0)
session.notes = request.POST.get('notes', '')
session.end_time = timezone.now()
session.status = 'closed'
session.save()
return redirect('index')
return render(request, 'core/close_session.html', {'session': session})
@login_required
def session_detail(request, pk): return render(request, 'core/session_detail.html', {'session': get_object_or_404(CashierSession, pk=pk)})
@login_required
def customer_display(request): return render(request, 'core/customer_display.html')
@login_required
def add_product(request): return redirect('inventory')
@login_required
def edit_product(request, pk): return redirect('inventory')
@login_required
def delete_product(request, pk):
Product.objects.filter(pk=pk).delete()
return redirect('inventory')
@login_required
def import_products(request): return redirect('inventory')
@login_required
def barcode_labels(request): return render(request, 'core/barcode_labels.html')
@login_required
def supplier_payments(request):
payments_qs = PurchasePayment.objects.all().select_related("purchase", "purchase__supplier", "payment_method", "created_by").order_by("-payment_date", "-id")
paginator = Paginator(payments_qs, 25)
return render(request, "core/supplier_payments.html", {"payments": paginator.get_page(request.GET.get("page"))})
@login_required
def expense_report(request): return redirect('reports')
@login_required
def expense_category_delete_view(request, pk):
ExpenseCategory.objects.filter(pk=pk).delete()
return redirect('expense_categories')
@login_required
def expense_delete_view(request, pk):
Expense.objects.filter(pk=pk).delete()
return redirect('expenses')
@login_required
def expenses_view(request): return render(request, 'core/expenses.html', {'expenses': Expense.objects.all().order_by('-date')})
@login_required @login_required
def expense_create_view(request): return redirect('expenses') def expense_create_view(request): return redirect('expenses')
@login_required @login_required
def expense_delete_view(request, pk): return redirect('expenses')
@login_required
def expense_categories_view(request): return render(request, 'core/expense_categories.html') def expense_categories_view(request): return render(request, 'core/expense_categories.html')
@login_required @login_required
def user_management(request): return render(request, 'core/users.html', {'users': User.objects.all()}) def expense_category_delete_view(request, pk): return redirect('expense_categories')
@login_required @login_required
def profile_view(request): return render(request, 'core/profile.html') def expense_report(request): return render(request, 'core/expense_report.html')
@login_required
def customer_payments(request): return redirect('invoices')
@login_required
def customer_payment_receipt(request, pk): return redirect('invoices')
@login_required
def sale_receipt(request, pk): return redirect('invoices')
@login_required
def edit_invoice(request, pk): return redirect('invoices')
@login_required @login_required
def add_sale_payment(request, pk): return redirect('invoices') def add_sale_payment(request, pk): return redirect('invoices')
@login_required @login_required
def delete_sale(request, pk): return redirect('invoices') def delete_sale(request, pk): return redirect('invoices')
@login_required @login_required
def edit_invoice(request, pk): return redirect('invoices') def supplier_payments(request): return redirect('purchases')
@login_required
def settings_view(request): return render(request, 'core/settings.html')
@login_required
def profile_view(request): return render(request, 'core/profile.html')
@login_required
def user_management(request): return render(request, 'core/users.html')
@csrf_exempt
def group_details_api(request, pk): return JsonResponse({'success': False})
@csrf_exempt
def create_sale_api(request):
if request.method != 'POST':
return JsonResponse({'success': False, 'error': 'Invalid request method'})
try:
data = json.loads(request.body)
customer_id = data.get('customer_id')
items = data.get('items', [])
discount = decimal.Decimal(str(data.get('discount', 0)))
paid_amount = decimal.Decimal(str(data.get('paid_amount', 0)))
payment_type = data.get('payment_type', 'cash')
payment_method_id = data.get('payment_method_id')
due_date = data.get('due_date')
notes = data.get('notes', '')
invoice_number = data.get('invoice_number')
if not items:
return JsonResponse({'success': False, 'error': 'No items in sale'})
with transaction.atomic():
customer = None
if customer_id:
customer = Customer.objects.get(pk=customer_id)
# Calculate totals server-side for security
subtotal = decimal.Decimal(0)
vat_amount = decimal.Decimal(0)
sale = Sale(
customer=customer,
created_by=request.user,
payment_status='pending',
discount=discount,
notes=notes,
invoice_number=invoice_number
)
if due_date:
sale.due_date = due_date
sale.save()
for item in items:
product = Product.objects.select_for_update().get(pk=item['id'])
qty = decimal.Decimal(str(item['quantity']))
unit_price = decimal.Decimal(str(item['price']))
# Check stock
if product.stock_quantity < qty:
settings = SystemSetting.objects.first()
if not settings or not settings.allow_zero_stock_sales:
raise Exception(f"Insufficient stock for {product.name_en}")
line_total = unit_price * qty
line_vat = line_total * (product.vat / 100) if product.vat else 0
SaleItem.objects.create(
sale=sale,
product=product,
quantity=qty,
unit_price=unit_price,
line_total=line_total
)
product.stock_quantity -= qty
product.save()
subtotal += line_total
vat_amount += decimal.Decimal(line_vat)
sale.subtotal = subtotal
sale.vat_amount = vat_amount
sale.total_amount = subtotal + vat_amount - discount
if payment_type == 'credit':
sale.payment_status = 'unpaid'
elif paid_amount >= sale.total_amount:
sale.payment_status = 'paid'
else:
sale.payment_status = 'partial'
sale.save()
if paid_amount > 0 and payment_type != 'credit':
payment_method = None
if payment_method_id:
payment_method = PaymentMethod.objects.get(pk=payment_method_id)
SalePayment.objects.create(
sale=sale,
amount=paid_amount,
payment_method=payment_method,
payment_date=timezone.now(),
created_by=request.user,
notes=f"Initial payment ({payment_type})"
)
return JsonResponse({'success': True, 'sale_id': sale.id})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)})
@csrf_exempt
def create_purchase_api(request): return JsonResponse({'success': False})
@csrf_exempt
def search_customers_api(request): return JsonResponse({'customers': []})
@login_required
def pos_sync_update(request): return JsonResponse({'success': False})
@login_required
def pos_sync_state(request): return JsonResponse({'success': False})