Autosave: 20260207-171221
This commit is contained in:
parent
5021756176
commit
c79ace1553
Binary file not shown.
@ -12,7 +12,7 @@
|
||||
<p class="text-muted small mb-0">{% trans "Welcome back! Here's what's happening with your business today." %}</p>
|
||||
</div>
|
||||
<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" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
733
core/views.py
733
core/views.py
@ -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 import timezone
|
||||
from django.contrib.auth.models import User
|
||||
@ -7,7 +7,8 @@ from django.dispatch import receiver
|
||||
import base64
|
||||
import os
|
||||
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 django.core.paginator import Paginator
|
||||
import decimal
|
||||
@ -38,7 +39,6 @@ from django.contrib import messages
|
||||
from django.utils.text import slugify
|
||||
import openpyxl
|
||||
import csv
|
||||
from . import views_import
|
||||
|
||||
@login_required
|
||||
def index(request):
|
||||
@ -50,271 +50,298 @@ def index(request):
|
||||
total_sales_amount = Sale.objects.aggregate(total=Sum('total_amount'))['total'] or 0
|
||||
total_customers = Customer.objects.count()
|
||||
|
||||
site_settings = SystemSetting.objects.first()
|
||||
|
||||
# --- Charts & Analytics Data ---
|
||||
|
||||
# 1. Monthly Sales (Last 6 months)
|
||||
today = timezone.now().date()
|
||||
expired_count = Product.objects.filter(has_expiry=True, expiry_date__lt=today, stock_quantity__gt=0).count()
|
||||
|
||||
low_stock_qs = Product.objects.filter(stock_quantity__lt=5)
|
||||
low_stock_count = low_stock_qs.count()
|
||||
low_stock_products = low_stock_qs[:5]
|
||||
six_months_ago = today - timedelta(days=180)
|
||||
|
||||
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')) \
|
||||
monthly_sales = 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')
|
||||
|
||||
|
||||
monthly_labels = []
|
||||
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']))
|
||||
|
||||
current_lang = get_language()
|
||||
|
||||
top_products_qs = SaleItem.objects.values('product__name_en', 'product__name_ar') \
|
||||
.annotate(total_qty=Sum('quantity'), total_rev=Sum('line_total')) \
|
||||
.order_by('-total_qty')[:5]
|
||||
for entry in monthly_sales:
|
||||
dt = entry['month']
|
||||
# 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)))
|
||||
|
||||
category_sales_qs = SaleItem.objects.values('product__category__name_en', 'product__category__name_ar') \
|
||||
.annotate(total=Sum('line_total')) \
|
||||
.order_by('-total')
|
||||
# 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_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']))
|
||||
|
||||
for item in category_sales:
|
||||
name_en = item['product__category__name_en'] or "Uncategorized"
|
||||
name_ar = item['product__category__name_ar'] or name_en
|
||||
|
||||
payment_stats_qs = SalePayment.objects.values('payment_method_name') \
|
||||
.annotate(total=Sum('amount')) \
|
||||
.order_by('-total')
|
||||
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_data = []
|
||||
for entry in payment_stats_qs:
|
||||
payment_labels.append(entry['payment_method_name'] or "Unknown")
|
||||
payment_data.append(float(entry['total']))
|
||||
|
||||
for stat in payment_stats:
|
||||
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 = {
|
||||
'total_products': total_products,
|
||||
'total_sales_count': total_sales_count,
|
||||
'total_sales_amount': total_sales_amount,
|
||||
'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_count': low_stock_count,
|
||||
'expired_count': expired_count,
|
||||
'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)
|
||||
|
||||
@login_required
|
||||
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')
|
||||
if category_id: products_list = products_list.filter(category_id=category_id)
|
||||
search = request.GET.get('search')
|
||||
if search:
|
||||
products_list = products_list.filter(Q(name_en__icontains=search) | Q(name_ar__icontains=search) | Q(sku__icontains=search))
|
||||
today = timezone.now().date()
|
||||
expired_products = Product.objects.filter(has_expiry=True, expiry_date__lt=today, stock_quantity__gt=0)
|
||||
expiring_soon_products = Product.objects.filter(has_expiry=True, expiry_date__gte=today, expiry_date__lte=today + timedelta(days=30), stock_quantity__gt=0)
|
||||
paginator = Paginator(products_list, 25)
|
||||
products = paginator.get_page(request.GET.get('page'))
|
||||
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}
|
||||
if category_id:
|
||||
products = products.filter(category_id=category_id)
|
||||
|
||||
# Filter by Search
|
||||
search_query = request.GET.get('search')
|
||||
if search_query:
|
||||
products = products.filter(
|
||||
Q(name_en__icontains=search_query) |
|
||||
Q(name_ar__icontains=search_query) |
|
||||
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)
|
||||
|
||||
@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
|
||||
def customers(request):
|
||||
customers_qs = Customer.objects.all().annotate(total_sales=Sum('sales__total_amount')).order_by('name')
|
||||
paginator = Paginator(customers_qs, 25)
|
||||
context = {'customers': paginator.get_page(request.GET.get('page'))}
|
||||
return render(request, 'core/customers.html', context)
|
||||
customers = Customer.objects.all().order_by('-id')
|
||||
paginator = Paginator(customers, 25)
|
||||
return render(request, 'core/customers.html', {'customers': paginator.get_page(request.GET.get('page'))})
|
||||
|
||||
@login_required
|
||||
def suppliers(request):
|
||||
suppliers_qs = Supplier.objects.all().order_by('name')
|
||||
paginator = Paginator(suppliers_qs, 25)
|
||||
context = {'suppliers': paginator.get_page(request.GET.get('page'))}
|
||||
return render(request, 'core/suppliers.html', context)
|
||||
suppliers = Supplier.objects.all().order_by('-id')
|
||||
paginator = Paginator(suppliers, 25)
|
||||
return render(request, 'core/suppliers.html', {'suppliers': paginator.get_page(request.GET.get('page'))})
|
||||
|
||||
@login_required
|
||||
def purchases(request):
|
||||
purchases_qs = Purchase.objects.all().select_related('supplier', 'created_by').order_by('-created_at')
|
||||
paginator = Paginator(purchases_qs, 25)
|
||||
purchases = Purchase.objects.all().order_by('-created_at')
|
||||
paginator = Paginator(purchases, 25)
|
||||
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
|
||||
def purchase_detail(request, 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
|
||||
def create_purchase_api(request):
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
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)
|
||||
def purchase_create(request):
|
||||
# Stub for purchase creation - redirects to list for now
|
||||
return redirect('purchases')
|
||||
|
||||
@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
|
||||
def reports(request):
|
||||
monthly_sales = Sale.objects.annotate(month=TruncMonth('created_at')).values('month').annotate(total=Sum('total_amount')).order_by('-month')[:12]
|
||||
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")})
|
||||
return render(request, 'core/reports.html')
|
||||
|
||||
@login_required
|
||||
def customer_statement(request):
|
||||
customers = Customer.objects.all().order_by('name')
|
||||
customers = Customer.objects.all()
|
||||
selected_customer = None
|
||||
sales = []
|
||||
customer_id = request.GET.get('customer')
|
||||
if customer_id:
|
||||
selected_customer = get_object_or_404(Customer, id=customer_id)
|
||||
sales = Sale.objects.filter(customer=selected_customer).order_by('-created_at')
|
||||
if request.GET.get('start_date'): sales = sales.filter(created_at__date__gte=request.GET.get('start_date'))
|
||||
if request.GET.get('end_date'): sales = sales.filter(created_at__date__lte=request.GET.get('end_date'))
|
||||
return render(request, 'core/customer_statement.html', {'customers': customers, 'selected_customer': selected_customer, 'sales': sales})
|
||||
transactions = []
|
||||
|
||||
if request.GET.get('customer'):
|
||||
selected_customer = get_object_or_404(Customer, pk=request.GET.get('customer'))
|
||||
# Gather sales, payments, etc.
|
||||
sales = Sale.objects.filter(customer=selected_customer).annotate(type=models.Value('Sale', output_field=models.CharField()))
|
||||
payments = SalePayment.objects.filter(sale__customer=selected_customer).annotate(type=models.Value('Payment', output_field=models.CharField()))
|
||||
# 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
|
||||
def supplier_statement(request):
|
||||
suppliers = Supplier.objects.all().order_by('name')
|
||||
selected_supplier = None
|
||||
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})
|
||||
suppliers = Supplier.objects.all()
|
||||
return render(request, 'core/supplier_statement.html', {'suppliers': suppliers})
|
||||
|
||||
@login_required
|
||||
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()})
|
||||
|
||||
@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 ---
|
||||
@login_required
|
||||
@ -423,6 +467,8 @@ def delete_category(request, pk): return redirect('inventory')
|
||||
@csrf_exempt
|
||||
def add_category_ajax(request): return JsonResponse({'success': False})
|
||||
@login_required
|
||||
def import_categories(request): return redirect('inventory')
|
||||
@login_required
|
||||
def add_unit(request): return redirect('inventory')
|
||||
@login_required
|
||||
def edit_unit(request, pk): return redirect('inventory')
|
||||
@ -445,139 +491,180 @@ def edit_loyalty_tier(request, pk): return redirect('settings')
|
||||
@login_required
|
||||
def delete_loyalty_tier(request, pk): return redirect('settings')
|
||||
@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
|
||||
def send_invoice_whatsapp(request): return JsonResponse({'success': False})
|
||||
@csrf_exempt
|
||||
def group_details_api(request, pk): return JsonResponse({'users': []})
|
||||
def test_whatsapp_connection(request): return JsonResponse({'success': False})
|
||||
@login_required
|
||||
def search_customers_api(request):
|
||||
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)})
|
||||
def add_device(request): return redirect('settings')
|
||||
@login_required
|
||||
def customer_payments(request):
|
||||
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'))})
|
||||
def edit_device(request, pk): return redirect('settings')
|
||||
@login_required
|
||||
def customer_payment_receipt(request, pk):
|
||||
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)})
|
||||
def delete_device(request, pk): return redirect('settings')
|
||||
@login_required
|
||||
def sale_receipt(request, pk):
|
||||
return render(request, 'core/sale_receipt.html', {'sale': get_object_or_404(Sale, pk=pk), 'settings': SystemSetting.objects.first()})
|
||||
def lpo_list(request): return render(request, 'core/lpo_list.html')
|
||||
@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
|
||||
def pos_sync_update(request): return JsonResponse({'status': 'ok'})
|
||||
@csrf_exempt
|
||||
def pos_sync_state(request): return JsonResponse({'state': {}})
|
||||
def create_lpo_api(request): return JsonResponse({'success': False})
|
||||
@login_required
|
||||
def test_whatsapp_connection(request): return JsonResponse({'success': True, 'message': 'Connection simulation successful'})
|
||||
def cashier_registry(request): return redirect('settings')
|
||||
@login_required
|
||||
def add_device(request):
|
||||
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')
|
||||
def cashier_session_list(request): return render(request, 'core/cashier_sessions.html')
|
||||
@login_required
|
||||
def edit_device(request, pk):
|
||||
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')
|
||||
def start_session(request): return redirect('cashier_session_list')
|
||||
@login_required
|
||||
def delete_device(request, pk):
|
||||
get_object_or_404(Device, pk=pk).delete()
|
||||
messages.success(request, _("Device deleted successfully!"))
|
||||
return redirect(reverse('settings') + '#devices')
|
||||
def close_session(request): return redirect('cashier_session_list')
|
||||
@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
|
||||
def lpo_create(request): return render(request, 'core/lpo_create.html', {'suppliers': Supplier.objects.all(), 'products': Product.objects.filter(is_active=True)})
|
||||
@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')})
|
||||
def expenses_view(request): return render(request, 'core/expenses.html')
|
||||
@login_required
|
||||
def expense_create_view(request): return redirect('expenses')
|
||||
@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')
|
||||
@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
|
||||
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
|
||||
def add_sale_payment(request, pk): return redirect('invoices')
|
||||
@login_required
|
||||
def delete_sale(request, pk): return redirect('invoices')
|
||||
@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})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user