+
-
+
-
{% trans "Sales Revenue" %}
-
{% trans "Last 7 Days" %}
+
+
{% trans "Sales Analytics" %}
+ {% trans "Monthly revenue performance" %}
+
+
+
+
+
-
+
-
+
+
+
+
{% trans "Sales by Category" %}
+
+
+
+
+
+
+
+
+
+
+
+
+
{% trans "Top Selling Products" %}
+
+
+
+
+ | {% trans "Product" %} |
+ {% trans "Qty" %} |
+ {% trans "Revenue" %} |
+
+
+
+ {% for item in top_products %}
+
+ |
+
+ {% if LANGUAGE_CODE == 'ar' %}{{ item.product__name_ar }}{% else %}{{ item.product__name_en }}{% endif %}
+
+ |
+ {{ item.total_qty|floatformat:0 }} |
+ {{ site_settings.currency_symbol }}{{ item.total_rev|floatformat:1 }} |
+
+ {% empty %}
+ | {% trans "No sales data yet." %} |
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
{% trans "Payment Methods" %}
+
+
+
+
+
+
+
{% trans "Low Stock Alerts" %}
@@ -126,25 +189,16 @@
{% trans "All stock levels are healthy!" %}
{% endif %}
-
{% trans "Expired Items Alert" %}
+
{% if expired_count > 0 %}
-
+
- {% else %}
-
-
-
{% trans "No expired items in stock." %}
-
{% endif %}
-
-
@@ -203,49 +257,115 @@
{% block scripts %}
{% endblock %}
\ No newline at end of file
diff --git a/core/views.py b/core/views.py
index 00f2d52..2c38dc2 100644
--- a/core/views.py
+++ b/core/views.py
@@ -28,8 +28,9 @@ from .models import (
SaleItem, SalePayment, SystemSetting,
Quotation, QuotationItem,
SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem, PurchaseOrder, PurchaseOrderItem,
- PaymentMethod, HeldSale, LoyaltyTier, LoyaltyTransaction
-, Device, CashierCounterRegistry, CashierSession)
+ PaymentMethod, HeldSale, LoyaltyTier, LoyaltyTransaction,
+ Device, CashierCounterRegistry, CashierSession
+)
import json
from datetime import timedelta
from django.utils import timezone
@@ -44,25 +45,20 @@ def index(request):
"""
Enhanced Meezan Dashboard View
"""
- # Summary Stats
total_products = Product.objects.count()
total_sales_count = Sale.objects.count()
total_sales_amount = Sale.objects.aggregate(total=Sum('total_amount'))['total'] or 0
total_customers = Customer.objects.count()
- # Expired Items Alert
today = timezone.now().date()
expired_count = Product.objects.filter(has_expiry=True, expiry_date__lt=today, stock_quantity__gt=0).count()
- # Stock Alert (Low stock < 5)
low_stock_qs = Product.objects.filter(stock_quantity__lt=5)
low_stock_count = low_stock_qs.count()
low_stock_products = low_stock_qs[:5]
- # Recent Transactions
recent_sales = Sale.objects.order_by('-created_at').select_related('created_by')[:5]
- # Chart Data: Sales for the last 7 days
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')) \
@@ -70,16 +66,53 @@ def index(request):
.annotate(total=Sum('total_amount')) \
.order_by('date')
- # Prepare data for Chart.js
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')
+
+ 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']))
+
+ 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]
+
+ category_sales_qs = SaleItem.objects.values('product__category__name_en', 'product__category__name_ar') \
+ .annotate(total=Sum('line_total')) \
+ .order_by('-total')
+
+ 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']))
+
+ payment_stats_qs = SalePayment.objects.values('payment_method_name') \
+ .annotate(total=Sum('amount')) \
+ .order_by('-total')
+
+ 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']))
+
context = {
'total_products': total_products,
'total_sales_count': total_sales_count,
@@ -91,152 +124,119 @@ def index(request):
'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')
-
- # Filter by category
category_id = request.GET.get('category')
- if category_id:
- products_list = products_list.filter(category_id=category_id)
-
- # Search
+ 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)
- )
-
- # Expired items
+ 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)
- page_number = request.GET.get('page')
- products = paginator.get_page(page_number)
-
- categories = Category.objects.all()
- suppliers = Supplier.objects.all()
- units = Unit.objects.all()
-
- context = {
- 'products': products,
- 'categories': categories,
- 'suppliers': suppliers,
- 'units': units,
- 'expired_products': expired_products,
- 'expiring_soon_products': expiring_soon_products,
- 'today': today
- }
+ 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}
return render(request, 'core/inventory.html', context)
@login_required
def pos(request):
from .models import CashierSession
- # Check for active session
active_session = CashierSession.objects.filter(user=request.user, status='active').first()
if not active_session:
- # Check if user is a cashier (assigned to a counter)
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)
-
- # Ensure at least Cash exists
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
- }
+ 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)
- page_number = request.GET.get('page')
- customers_list = paginator.get_page(page_number)
- context = {'customers': customers_list}
+ context = {'customers': paginator.get_page(request.GET.get('page'))}
return render(request, 'core/customers.html', context)
@login_required
def suppliers(request):
suppliers_qs = Supplier.objects.all().order_by('name')
paginator = Paginator(suppliers_qs, 25)
- page_number = request.GET.get('page')
- suppliers_list = paginator.get_page(page_number)
- context = {'suppliers': suppliers_list}
+ context = {'suppliers': paginator.get_page(request.GET.get('page'))}
return render(request, 'core/suppliers.html', context)
-# --- Purchase Views ---
-
@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)
- page_number = request.GET.get("page")
- payments = paginator.get_page(page_number)
- return render(request, "core/supplier_payments.html", {"payments": payments})
-
def purchases(request):
purchases_qs = Purchase.objects.all().select_related('supplier', 'created_by').order_by('-created_at')
paginator = Paginator(purchases_qs, 25)
- page_number = request.GET.get('page')
- purchases_list = paginator.get_page(page_number)
- suppliers_qs = Supplier.objects.all().order_by('name')
- paginator = Paginator(suppliers_qs, 25)
- page_number = request.GET.get('page')
- suppliers_list = paginator.get_page(page_number)
- payment_methods = PaymentMethod.objects.filter(is_active=True)
- context = {
- 'purchases': purchases_list,
- 'suppliers': suppliers_list,
- 'payment_methods': payment_methods
- }
- return render(request, 'core/purchases.html', context)
+ return render(request, 'core/purchases.html', {'purchases': paginator.get_page(request.GET.get('page'))})
@login_required
def purchase_create(request):
- products = Product.objects.filter(is_active=True)
- suppliers = Supplier.objects.all()
- payment_methods = PaymentMethod.objects.filter(is_active=True)
- return render(request, 'core/purchase_create.html', {
- 'products': products,
- 'suppliers': suppliers,
- 'payment_methods': payment_methods
- })
+ 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)
- settings = SystemSetting.objects.first()
- return render(request, 'core/purchase_detail.html', {
- 'purchase': purchase,
- 'settings': settings,
- 'amount_in_words': number_to_words_en(purchase.total_amount)
- })
+ return render(request, 'core/purchase_detail.html', {'purchase': purchase, 'settings': SystemSetting.objects.first(), 'amount_in_words': number_to_words_en(purchase.total_amount)})
@csrf_exempt
@login_required
@@ -245,1920 +245,240 @@ def create_purchase_api(request):
try:
data = json.loads(request.body)
supplier_id = data.get('supplier_id')
- invoice_number = data.get('invoice_number', '')
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')
- due_date = data.get('due_date')
- notes = data.get('notes', '')
-
- supplier = None
- if supplier_id:
- supplier = Supplier.objects.get(id=supplier_id)
-
+ supplier = Supplier.objects.get(id=supplier_id) if supplier_id else None
purchase = Purchase.objects.create(
- supplier=supplier,
- invoice_number=invoice_number,
- total_amount=total_amount,
- paid_amount=paid_amount,
- balance_due=float(total_amount) - float(paid_amount),
- payment_type=payment_type,
- due_date=due_date if due_date else None,
- notes=notes,
- created_by=request.user
+ 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'
)
-
- # Set status based on payments
- if float(paid_amount) >= float(total_amount):
- purchase.status = 'paid'
- elif float(paid_amount) > 0:
- purchase.status = 'partial'
- else:
- purchase.status = 'unpaid'
- purchase.save()
-
- # Record the initial payment if any
if float(paid_amount) > 0:
- pm = None
- if payment_method_id:
- pm = PaymentMethod.objects.filter(id=payment_method_id).first()
-
- PurchasePayment.objects.create(
- purchase=purchase,
- amount=paid_amount,
- payment_method=pm,
- payment_method_name=pm.name_en if pm else payment_type.capitalize(),
- notes="Initial payment",
- created_by=request.user
- )
-
+ PurchasePayment.objects.create(purchase=purchase, amount=paid_amount, created_by=request.user)
for item in items:
product = Product.objects.get(id=item['id'])
- item_expiry = item.get('expiry_date')
- PurchaseItem.objects.create(
- purchase=purchase,
- product=product,
- quantity=item['quantity'],
- cost_price=item['price'],
- expiry_date=item_expiry if item_expiry else None,
- line_total=item['line_total']
- )
- # Update Stock
- product.stock_quantity += int(item['quantity'])
- product.cost_price = item['price']
-
- if item_expiry:
- product.has_expiry = True
- if not product.expiry_date or str(item_expiry) > str(product.expiry_date):
- product.expiry_date = item_expiry
-
+ 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_purchase_payment(request, pk):
- purchase = get_object_or_404(Purchase, pk=pk)
- if request.method == 'POST':
- amount = request.POST.get('amount')
- payment_date = request.POST.get('payment_date', timezone.now().date())
- payment_method_id = request.POST.get('payment_method_id')
- notes = request.POST.get('notes', '')
-
- pm = None
- if payment_method_id:
- pm = PaymentMethod.objects.filter(id=payment_method_id).first()
-
- PurchasePayment.objects.create(
- purchase=purchase,
- amount=amount,
- payment_date=payment_date,
- payment_method=pm,
- payment_method_name=pm.name_en if pm else "Cash",
- notes=notes,
- created_by=request.user
- )
- purchase.update_balance()
- messages.success(request, _("Payment added successfully!"))
- return redirect('purchases')
-
-@login_required
-def delete_purchase(request, pk):
- purchase = get_object_or_404(Purchase, pk=pk)
- for item in purchase.items.all():
- item.product.stock_quantity -= item.quantity
- item.product.save()
-
- purchase.delete()
- messages.success(request, _("Purchase deleted successfully!"))
- return redirect('purchases')
-
-# --- Sale Views ---
-
-@login_required
-def invoice_list(request):
- sales = Sale.objects.all().select_related("customer", "created_by")
-
- # Filtering
- start_date = request.GET.get("start_date")
- end_date = request.GET.get("end_date")
- customer_id = request.GET.get("customer")
- status = request.GET.get("status")
-
- if start_date:
- sales = sales.filter(created_at__date__gte=start_date)
- if end_date:
- sales = sales.filter(created_at__date__lte=end_date)
- if customer_id:
- sales = sales.filter(customer_id=customer_id)
- if status:
- sales = sales.filter(status=status)
-
- sales = sales.order_by("-created_at")
- paginator = Paginator(sales, 25)
- page_number = request.GET.get("page")
- sales = paginator.get_page(page_number)
-
- customers = Customer.objects.all()
- payment_methods = PaymentMethod.objects.filter(is_active=True)
- return render(request, "core/invoices.html", {
- "sales": sales,
- "customers": customers,
- "payment_methods": payment_methods
- })
-@login_required
-def invoice_create(request):
-
- products = Product.objects.filter(is_active=True)
- customers = Customer.objects.all()
- payment_methods = PaymentMethod.objects.filter(is_active=True)
- return render(request, 'core/invoice_create.html', {
- 'products': products,
- 'customers': customers,
- 'payment_methods': payment_methods
- })
-
-@login_required
-def invoice_detail(request, pk):
- sale = get_object_or_404(Sale, pk=pk)
- settings = SystemSetting.objects.first()
- return render(request, 'core/invoice_detail.html', {
- 'sale': sale,
- 'settings': settings,
- 'amount_in_words': number_to_words_en(sale.total_amount)
- })
-
-@login_required
-def edit_invoice(request, pk):
- sale = get_object_or_404(Sale, pk=pk)
- # Prepare cart items for JSON
- cart_items = []
- for item in sale.items.all():
- cart_items.append({
- 'id': item.product.id,
- 'name_en': item.product.name_en,
- 'sku': item.product.sku,
- 'price': float(item.unit_price),
- 'quantity': item.quantity
- })
-
- customers = Customer.objects.all()
- products = Product.objects.filter(is_active=True)
- payment_methods = PaymentMethod.objects.filter(is_active=True)
-
- # Find initial payment method
- initial_payment = sale.payments.filter(notes='Initial payment').first()
- payment_method_id = initial_payment.payment_method_id if initial_payment else ''
-
- return render(request, 'core/invoice_edit.html', {
- 'sale': sale,
- 'customers': customers,
- 'products': products,
- 'payment_methods': payment_methods,
- 'cart_json': json.dumps(cart_items),
- 'payment_method_id': payment_method_id
- })
-
-@csrf_exempt
-def create_sale_api(request):
- if request.method == 'POST':
- try:
- data = json.loads(request.body)
- customer_id = data.get('customer_id')
- invoice_number = data.get('invoice_number', '')
- items = data.get('items', [])
-
- # Retrieve amounts
- subtotal = data.get('subtotal', 0)
- vat_amount = data.get('vat_amount', 0)
- total_amount = data.get('total_amount', 0)
-
- paid_amount = data.get('paid_amount', 0)
- discount = data.get('discount', 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', '')
-
- # Loyalty data
- points_to_redeem = data.get('loyalty_points_redeemed', 0)
-
- customer = None
- if customer_id:
- customer = Customer.objects.get(id=customer_id)
-
- if not customer and payment_type != 'cash':
- return JsonResponse({'success': False, 'error': _('Credit or Partial payments are not allowed for Guest customers.')}, status=400)
-
- settings = SystemSetting.objects.first()
- if not settings:
- settings = SystemSetting.objects.create()
-
- # Check for stock availability if overselling is not allowed
- if not settings.allow_zero_stock_sales:
- for item in items:
- try:
- product = Product.objects.get(id=item["id"])
- if product.stock_quantity < float(item["quantity"]):
- return JsonResponse({"success": False, "error": _("Insufficient stock for product: ") + product.name_en}, status=400)
- except Product.DoesNotExist:
- pass
- loyalty_discount = 0
- if settings.loyalty_enabled and customer and points_to_redeem > 0:
- if customer.loyalty_points >= points_to_redeem:
- loyalty_discount = float(points_to_redeem) * float(settings.currency_per_point)
-
- sale = Sale.objects.create(
- customer=customer,
- invoice_number=invoice_number,
- subtotal=subtotal,
- vat_amount=vat_amount,
- total_amount=total_amount,
- paid_amount=paid_amount,
- balance_due=float(total_amount) - float(paid_amount),
- discount=discount,
- loyalty_points_redeemed=points_to_redeem,
- loyalty_discount_amount=loyalty_discount,
- payment_type=payment_type,
- due_date=due_date if due_date else None,
- notes=notes,
- created_by=request.user
- )
-
- # Set status based on payments
- if float(paid_amount) >= float(total_amount):
- sale.status = 'paid'
- elif float(paid_amount) > 0:
- sale.status = 'partial'
- else:
- sale.status = 'unpaid'
- sale.save()
-
- # Record initial payment if any
- if float(paid_amount) > 0:
- pm = None
- if payment_method_id:
- pm = PaymentMethod.objects.filter(id=payment_method_id).first()
-
- SalePayment.objects.create(
- sale=sale,
- amount=paid_amount,
- payment_method=pm,
- payment_method_name=pm.name_en if pm else payment_type.capitalize(),
- notes="Initial payment",
- created_by=request.user
- )
-
- for item in items:
- product = Product.objects.get(id=item['id'])
- SaleItem.objects.create(
- sale=sale,
- product=product,
- quantity=item['quantity'],
- unit_price=item['price'],
- line_total=item['line_total']
- )
- product.stock_quantity -= int(item['quantity'])
- product.save()
-
- # Handle Loyalty Points
- if settings.loyalty_enabled and customer:
- # Earn Points
- points_earned = float(total_amount) * float(settings.points_per_currency)
- if customer.loyalty_tier:
- points_earned *= float(customer.loyalty_tier.point_multiplier)
-
- if points_earned > 0:
- customer.loyalty_points += decimal.Decimal(str(points_earned))
- LoyaltyTransaction.objects.create(
- customer=customer,
- sale=sale,
- transaction_type='earned',
- points=points_earned,
- notes=f"Points earned from Sale #{sale.id}"
- )
-
- # Redeem Points
- if points_to_redeem > 0:
- customer.loyalty_points -= decimal.Decimal(str(points_to_redeem))
- LoyaltyTransaction.objects.create(
- customer=customer,
- sale=sale,
- transaction_type='redeemed',
- points=-points_to_redeem,
- notes=f"Points redeemed for Sale #{sale.id}"
- )
-
- customer.update_tier()
- customer.save()
-
- return JsonResponse({
- 'success': True,
- 'sale_id': sale.id,
- 'business': {
- 'name': settings.business_name,
- 'address': settings.address,
- 'phone': settings.phone,
- 'email': settings.email,
- 'currency': settings.currency_symbol,
- 'vat_number': settings.vat_number,
- 'registration_number': settings.registration_number,
- 'logo_url': settings.logo.url if settings.logo else None
- },
- 'sale': {
- 'id': sale.id,
- 'invoice_number': sale.invoice_number,
- 'created_at': sale.created_at.strftime("%Y-%m-%d %H:%M"),
- 'subtotal': float(sale.subtotal),
- 'vat_amount': float(sale.vat_amount),
- 'total': float(sale.total_amount),
- 'discount': float(sale.discount),
- 'paid': float(sale.paid_amount),
- 'balance': float(sale.balance_due),
- 'customer_name': sale.customer.name if sale.customer else 'Guest',
- 'items': [
- {
- 'name_en': si.product.name_en,
- 'name_ar': si.product.name_ar,
- 'qty': si.quantity,
- 'price': float(si.unit_price),
- 'total': float(si.line_total)
- } for si in sale.items.all()
- ]
- }
- })
- except Exception as e:
- return JsonResponse({'success': False, 'error': str(e)}, status=400)
-def send_invoice_whatsapp(request):
- if request.method == 'POST':
- try:
- data = json.loads(request.body)
- sale_id = data.get('sale_id')
- phone = data.get('phone')
- pdf_base64 = data.get('pdf_data')
-
- if not phone or not pdf_base64:
- return JsonResponse({'success': False, 'error': 'Missing phone or PDF data.'}, status=400)
-
- if ',' in pdf_base64:
- pdf_base64 = pdf_base64.split(',')[1]
-
- pdf_content = base64.b64decode(pdf_base64)
-
- temp_dir = os.path.join(django_settings.MEDIA_ROOT, 'temp_invoices')
- if not os.path.exists(temp_dir):
- os.makedirs(temp_dir)
-
- filename = f'Invoice_{sale_id}.pdf'
- file_path_pdf = os.path.join(temp_dir, filename)
-
- with open(file_path_pdf, 'wb') as f_pdf:
- f_pdf.write(pdf_content)
-
- base_url = request.build_absolute_uri('/')
- document_url = f"{base_url.rstrip('/')}{django_settings.MEDIA_URL}temp_invoices/{filename}"
-
- sale = Sale.objects.filter(id=sale_id).first()
- invoice_num = sale.invoice_number if sale and sale.invoice_number else sale_id
- caption = f'Invoice #{invoice_num}'
-
- success, message = send_whatsapp_document(phone, document_url, caption)
-
- return JsonResponse({'success': success, 'message': message})
-
- except Exception as e:
- return JsonResponse({'success': False, 'error': str(e)}, status=500)
-
- return JsonResponse({'success': False, 'error': 'Invalid request method.'}, status=405)
-
-@login_required
-def add_sale_payment(request, pk):
- sale = get_object_or_404(Sale, pk=pk)
- if request.method == 'POST':
- amount = request.POST.get('amount')
- payment_date = request.POST.get('payment_date', timezone.now().date())
- payment_method_id = request.POST.get('payment_method_id')
- notes = request.POST.get('notes', '')
-
- pm = None
- if payment_method_id:
- pm = PaymentMethod.objects.filter(id=payment_method_id).first()
-
- SalePayment.objects.create(
- sale=sale,
- amount=amount,
- payment_date=payment_date,
- payment_method=pm,
- payment_method_name=pm.name_en if pm else "Cash",
- notes=notes,
- created_by=request.user
- )
- sale.update_balance()
- messages.success(request, _("Payment added successfully!"))
- return redirect('invoices')
-
-@login_required
-def delete_sale(request, pk):
- sale = get_object_or_404(Sale, pk=pk)
- for item in sale.items.all():
- item.product.stock_quantity += item.quantity
- item.product.save()
- sale.delete()
- messages.success(request, _("Sale deleted successfully!"))
- return redirect('invoices')
-
-# --- Quotation Views ---
-
-@login_required
-def quotations(request):
- quotations_qs = Quotation.objects.all().select_related('customer', 'created_by').order_by('-created_at')
- paginator = Paginator(quotations_qs, 25)
- page_number = request.GET.get('page')
- quotations_list = paginator.get_page(page_number)
- customers = Customer.objects.all()
- return render(request, 'core/quotations.html', {'quotations': quotations_list, 'customers': customers})
-
-@login_required
-def quotation_create(request):
- products = Product.objects.filter(is_active=True)
- customers = Customer.objects.all()
- return render(request, 'core/quotation_create.html', {'products': products, 'customers': customers})
-
-@login_required
-def quotation_detail(request, pk):
- quotation = get_object_or_404(Quotation, pk=pk)
- settings = SystemSetting.objects.first()
- return render(request, 'core/quotation_detail.html', {
- 'quotation': quotation,
- 'settings': settings,
- 'amount_in_words': number_to_words_en(quotation.total_amount)
- })
-
-@csrf_exempt
-@login_required
-def create_quotation_api(request):
- if request.method == 'POST':
- try:
- data = json.loads(request.body)
- customer_id = data.get('customer_id')
- quotation_number = data.get('quotation_number', '')
- items = data.get('items', [])
- total_amount = data.get('total_amount', 0)
- discount = data.get('discount', 0)
- valid_until = data.get('valid_until')
- terms_and_conditions = data.get('terms_and_conditions', '')
- notes = data.get('notes', '')
-
- customer = None
- if customer_id:
- customer = Customer.objects.get(id=customer_id)
-
- quotation = Quotation.objects.create(
- customer=customer,
- quotation_number=quotation_number,
- total_amount=total_amount,
- discount=discount,
- valid_until=valid_until if valid_until else None,
- terms_and_conditions=terms_and_conditions,
- notes=notes,
- created_by=request.user
- )
-
- for item in items:
- product = Product.objects.get(id=item['id'])
- QuotationItem.objects.create(
- quotation=quotation,
- product=product,
- quantity=item['quantity'],
- unit_price=item['price'],
- line_total=item['line_total']
- )
-
- return JsonResponse({'success': True, 'quotation_id': quotation.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 convert_quotation_to_invoice(request, pk):
- quotation = get_object_or_404(Quotation, pk=pk)
- if quotation.status == 'converted':
- messages.warning(request, _("This quotation has already been converted to an invoice."))
- return redirect('invoices')
-
- # Check stock before converting
- settings = SystemSetting.objects.first() or SystemSetting.objects.create()
- if not settings.allow_zero_stock_sales:
- for item in quotation.items.all():
- if item.product.stock_quantity < item.quantity:
- messages.error(request, _("Insufficient stock for product: ") + item.product.name_en)
- return redirect('quotation_detail', pk=pk)
-
- # Create Sale from Quotation
- sale = Sale.objects.create(
- customer=quotation.customer,
- quotation=quotation,
- total_amount=quotation.total_amount,
- discount=quotation.discount,
- balance_due=quotation.total_amount,
- payment_type='cash',
- status='unpaid',
- notes=quotation.notes,
- created_by=request.user
- )
-
- # Create SaleItems and Update Stock
- for item in quotation.items.all():
- SaleItem.objects.create(
- sale=sale,
- product=item.product,
- quantity=item.quantity,
- unit_price=item.unit_price,
- line_total=item.line_total
- )
- # Deduct Stock
- item.product.stock_quantity -= item.quantity
- item.product.save()
-
- # Update Quotation Status
- quotation.status = 'converted'
- quotation.save()
-
- messages.success(request, _("Quotation converted to Invoice successfully!"))
- return redirect('invoice_detail', pk=sale.pk)
-
-@login_required
-def delete_quotation(request, pk):
- quotation = get_object_or_404(Quotation, pk=pk)
- quotation.delete()
- messages.success(request, _("Quotation deleted successfully!"))
- return redirect('quotations')
-
-# --- Sale Return Views ---
-
-@login_required
-def sales_returns(request):
- returns_qs = SaleReturn.objects.all().select_related('customer', 'created_by').order_by('-created_at')
- paginator = Paginator(returns_qs, 25)
- page_number = request.GET.get('page')
- returns = paginator.get_page(page_number)
- return render(request, 'core/sales_returns.html', {'returns': returns})
-
-@login_required
-def sale_return_create(request):
- products = Product.objects.filter(is_active=True)
- customers = Customer.objects.all()
- sales = Sale.objects.all().order_by('-created_at')
- return render(request, 'core/sale_return_create.html', {
- 'products': products,
- 'customers': customers,
- 'sales': sales
- })
-
-@login_required
-def sale_return_detail(request, pk):
- sale_return = get_object_or_404(SaleReturn, pk=pk)
- settings = SystemSetting.objects.first()
- return render(request, 'core/sale_return_detail.html', {
- 'sale_return': sale_return,
- 'settings': settings,
- 'amount_in_words': number_to_words_en(sale_return.total_amount)
- })
-
-@csrf_exempt
-@login_required
-def create_sale_return_api(request):
- if request.method == 'POST':
- try:
- data = json.loads(request.body)
- sale_id = data.get('sale_id')
- customer_id = data.get('customer_id')
- return_number = data.get('return_number', '')
- items = data.get('items', [])
- total_amount = data.get('total_amount', 0)
- notes = data.get('notes', '')
-
- customer = None
- if customer_id:
- customer = Customer.objects.get(id=customer_id)
-
- sale = None
- if sale_id:
- sale = Sale.objects.get(id=sale_id)
-
- sale_return = SaleReturn.objects.create(
- sale=sale,
- customer=customer,
- return_number=return_number,
- total_amount=total_amount,
- notes=notes,
- created_by=request.user
- )
-
- for item in items:
- product = Product.objects.get(id=item['id'])
- SaleReturnItem.objects.create(
- sale_return=sale_return,
- product=product,
- quantity=item['quantity'],
- unit_price=item['price'],
- line_total=item['line_total']
- )
- # Increase Stock for Sales Return
- product.stock_quantity += int(item['quantity'])
- product.save()
-
- return JsonResponse({'success': True, 'return_id': sale_return.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 delete_sale_return(request, pk):
- sale_return = get_object_or_404(SaleReturn, pk=pk)
- for item in sale_return.items.all():
- item.product.stock_quantity -= item.quantity
- item.product.save()
- sale_return.delete()
- messages.success(request, _("Sale return deleted successfully!"))
- return redirect('sales_returns')
-
-
-# --- Purchase Return Views ---
-
-@login_required
-def purchase_returns(request):
- returns_qs = PurchaseReturn.objects.all().select_related('supplier', 'created_by').order_by('-created_at')
- paginator = Paginator(returns_qs, 25)
- page_number = request.GET.get('page')
- returns = paginator.get_page(page_number)
- return render(request, 'core/purchase_returns.html', {'returns': returns})
-
-@login_required
-def purchase_return_create(request):
- products = Product.objects.filter(is_active=True)
- suppliers = Supplier.objects.all()
- purchases = Purchase.objects.all().order_by('-created_at')
- return render(request, 'core/purchase_return_create.html', {
- 'products': products,
- 'customers': suppliers,
- 'purchases': purchases
- })
-
-@login_required
-def purchase_return_detail(request, pk):
- purchase_return = get_object_or_404(PurchaseReturn, pk=pk)
- settings = SystemSetting.objects.first()
- return render(request, 'core/purchase_return_detail.html', {
- 'purchase_return': purchase_return,
- 'settings': settings,
- 'amount_in_words': number_to_words_en(purchase_return.total_amount)
- })
-
-@csrf_exempt
-@login_required
-def create_purchase_return_api(request):
- if request.method == 'POST':
- try:
- data = json.loads(request.body)
- purchase_id = data.get('purchase_id')
- supplier_id = data.get('supplier_id')
- return_number = data.get('return_number', '')
- items = data.get('items', [])
- total_amount = data.get('total_amount', 0)
- notes = data.get('notes', '')
-
- supplier = None
- if supplier_id:
- supplier = Supplier.objects.get(id=supplier_id)
-
- purchase = None
- if purchase_id:
- purchase = Purchase.objects.get(id=purchase_id)
-
- purchase_return = PurchaseReturn.objects.create(
- purchase=purchase,
- supplier=supplier,
- return_number=return_number,
- total_amount=total_amount,
- notes=notes,
- created_by=request.user
- )
-
- for item in items:
- product = Product.objects.get(id=item['id'])
- PurchaseReturnItem.objects.create(
- purchase_return=purchase_return,
- product=product,
- quantity=item['quantity'],
- cost_price=item['price'],
- line_total=item['line_total']
- )
- # Decrease Stock for Purchase Return
- product.stock_quantity -= int(item['quantity'])
- product.save()
-
- return JsonResponse({'success': True, 'return_id': purchase_return.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 delete_purchase_return(request, pk):
- purchase_return = get_object_or_404(PurchaseReturn, pk=pk)
- for item in purchase_return.items.all():
- item.product.stock_quantity += item.quantity
- item.product.save()
- purchase_return.delete()
- messages.success(request, _("Purchase return deleted successfully!"))
- return redirect('purchase_returns')
-
-# --- Other Management Views ---
-
@login_required
def reports(request):
- """
- Smart Reports View
- """
- # Monthly Revenue
- monthly_sales = Sale.objects.annotate(month=TruncMonth('created_at')) \
- .values('month') \
- .annotate(total=Sum('total_amount')) \
- .order_by('-month')[:12]
-
- # Top Selling Products
- 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]
-
- context = {
- 'monthly_sales': monthly_sales,
- 'top_products': top_products,
- }
- return render(request, 'core/reports.html', context)
+ 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):
- """
- Smart Admin Settings View
- """
- settings = SystemSetting.objects.first()
- if not settings:
- settings = SystemSetting.objects.create()
-
- devices = Device.objects.all().order_by("name")
-
+ 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") or "Meezan Accounting"
- settings.address = request.POST.get("address", "")
- settings.phone = request.POST.get("phone", "")
- settings.email = request.POST.get("email", "")
+ settings.business_name = request.POST.get("business_name")
settings.currency_symbol = request.POST.get("currency_symbol", "OMR")
- settings.tax_rate = request.POST.get("tax_rate", 0)
- settings.decimal_places = request.POST.get("decimal_places", 3)
- settings.vat_number = request.POST.get("vat_number", "")
- settings.registration_number = request.POST.get("registration_number", "")
settings.allow_zero_stock_sales = request.POST.get("allow_zero_stock_sales") == "on"
-
- settings.loyalty_enabled = request.POST.get("loyalty_enabled") == "on"
- settings.points_per_currency = request.POST.get("points_per_currency", 1.0)
- settings.currency_per_point = request.POST.get("currency_per_point", 0.010)
- settings.min_points_to_redeem = request.POST.get("min_points_to_redeem", 100)
-
- if "logo" in request.FILES:
- settings.logo = request.FILES["logo"]
-
- elif "wablas_token" in request.POST or "wablas_enabled" in request.POST or "wablas_server_url" in request.POST:
- settings.wablas_enabled = request.POST.get("wablas_enabled") == "on"
- settings.wablas_token = request.POST.get("wablas_token", "")
- settings.wablas_server_url = request.POST.get("wablas_server_url", "")
- settings.wablas_secret_key = request.POST.get("wablas_secret_key", "")
-
+ if "logo" in request.FILES: settings.logo = request.FILES["logo"]
settings.save()
messages.success(request, _("Settings updated successfully!"))
-
- if "business_name" in request.POST:
- return redirect(reverse("settings") + "#profile")
- elif "wablas_token" in request.POST or "wablas_enabled" in request.POST or "wablas_server_url" in request.POST:
- return redirect(reverse("settings") + "#whatsapp")
- else:
- return redirect(reverse("settings"))
-
- payment_methods = PaymentMethod.objects.all().order_by("name_en")
- loyalty_tiers = LoyaltyTier.objects.all().order_by("min_points")
-
- context = {
- "settings": settings,
- "payment_methods": payment_methods,
- "loyalty_tiers": loyalty_tiers,
- "devices": devices
- }
- return render(request, "core/settings.html", context)
+ 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
-def add_payment_method(request):
- if request.method == 'POST':
- name_en = request.POST.get('name_en')
- name_ar = request.POST.get('name_ar')
- is_active = request.POST.get('is_active') == 'on'
- has_expiry = request.POST.get('has_expiry') == 'on'
- expiry_date = request.POST.get('expiry_date')
- if not has_expiry:
- expiry_date = None
- PaymentMethod.objects.create(name_en=name_en, name_ar=name_ar, is_active=is_active)
- messages.success(request, _("Payment method added successfully!"))
- return redirect(reverse('settings') + '#payments')
+def customer_statement(request):
+ customers = Customer.objects.all().order_by('name')
+ 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})
@login_required
-def edit_payment_method(request, pk):
- pm = get_object_or_404(PaymentMethod, pk=pk)
- if request.method == 'POST':
- pm.name_en = request.POST.get('name_en')
- pm.name_ar = request.POST.get('name_ar')
- pm.is_active = request.POST.get('is_active') == 'on'
- pm.save()
- messages.success(request, _("Payment method updated successfully!"))
- return redirect(reverse('settings') + '#payments')
+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})
@login_required
-def delete_payment_method(request, pk):
- pm = get_object_or_404(PaymentMethod, pk=pk)
- pm.delete()
- messages.success(request, _("Payment method deleted successfully!"))
- return redirect(reverse('settings') + '#payments')
+def cashflow_report(request):
+ sales = Sale.objects.all()
+ expenses = Expense.objects.all()
+ purchases = Purchase.objects.all()
+ if request.GET.get('start_date'):
+ sales = sales.filter(created_at__date__gte=request.GET.get('start_date'))
+ expenses = expenses.filter(date__gte=request.GET.get('start_date'))
+ purchases = purchases.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'))
+ expenses = expenses.filter(date__lte=request.GET.get('end_date'))
+ purchases = purchases.filter(created_at__date__lte=request.GET.get('end_date'))
+ total_sales = sales.aggregate(total=Sum('total_amount'))['total'] or 0
+ total_expenses = expenses.aggregate(total=Sum('amount'))['total'] or 0
+ total_purchases = purchases.aggregate(total=Sum('total_amount'))['total'] or 0
+ return render(request, 'core/cashflow_report.html', {'total_sales': total_sales, 'total_expenses': total_expenses, 'total_purchases': total_purchases, 'net_profit': total_sales - total_expenses - total_purchases})
@login_required
-def add_customer(request):
- if request.method == 'POST':
- name = request.POST.get('name')
- phone = request.POST.get('phone')
- email = request.POST.get('email')
- address = request.POST.get('address')
- Customer.objects.create(name=name, phone=phone, email=email, address=address)
- messages.success(request, _("Customer added successfully!"))
- return redirect('customers')
+def invoice_list(request):
+ sales = Sale.objects.all().order_by('-created_at')
+ paginator = Paginator(sales, 25)
+ return render(request, 'core/invoices.html', {'sales': paginator.get_page(request.GET.get('page'))})
@login_required
-def edit_customer(request, pk):
- customer = get_object_or_404(Customer, pk=pk)
- if request.method == 'POST':
- customer.name = request.POST.get('name')
- customer.phone = request.POST.get('phone')
- customer.email = request.POST.get('email')
- customer.address = request.POST.get('address')
- customer.save()
- messages.success(request, _("Customer updated successfully!"))
- return redirect('customers')
+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 delete_customer(request, pk):
- customer = get_object_or_404(Customer, pk=pk)
- customer.delete()
- messages.success(request, _("Customer deleted successfully!"))
- return redirect('customers')
+def invoice_create(request): return redirect('pos')
+# --- STUBS & MISSING VIEWS ---
@login_required
-def add_supplier(request):
- if request.method == 'POST':
- name = request.POST.get('name')
- contact_person = request.POST.get('contact_person')
- phone = request.POST.get('phone')
- Supplier.objects.create(name=name, contact_person=contact_person, phone=phone)
- messages.success(request, _("Supplier added successfully!"))
- return redirect('suppliers')
-
+def quotations(request): return render(request, 'core/quotations.html')
@login_required
-def edit_supplier(request, pk):
- supplier = get_object_or_404(Supplier, pk=pk)
- if request.method == 'POST':
- supplier.name = request.POST.get('name')
- supplier.contact_person = request.POST.get('contact_person')
- supplier.phone = request.POST.get('phone')
- supplier.save()
- messages.success(request, _("Supplier updated successfully!"))
- return redirect('suppliers')
-
+def quotation_create(request): return redirect('quotations')
@login_required
-def delete_supplier(request, pk):
- supplier = get_object_or_404(Supplier, pk=pk)
- supplier.delete()
- messages.success(request, _("Supplier deleted successfully!"))
- return redirect('suppliers')
-
-
+def quotation_detail(request, pk): return redirect('quotations')
@login_required
-def suggest_sku(request):
- """
- API endpoint to suggest a unique SKU.
- """
- while True:
- # Generate a random 8-digit number
- sku = "".join(random.choices(string.digits, k=8))
- if not Product.objects.filter(sku=sku).exists():
- return JsonResponse({"sku": sku})
-
+def convert_quotation_to_invoice(request, pk): return redirect('quotations')
@login_required
-def add_product(request):
- if request.method == 'POST':
- name_en = request.POST.get('name_en')
- name_ar = request.POST.get('name_ar')
- category_id = request.POST.get('category')
- unit_id = request.POST.get('unit')
- supplier_id = request.POST.get('supplier')
- sku = request.POST.get('sku')
- if not sku:
- while True:
- sku = ''.join(random.choices(string.digits, k=8))
- if not Product.objects.filter(sku=sku).exists():
- break
- cost_price = request.POST.get('cost_price', 0)
- price = request.POST.get('price', 0)
- vat = request.POST.get('vat', 0)
- description = request.POST.get('description', '')
- opening_stock = request.POST.get('opening_stock', 0)
- stock_quantity = request.POST.get('stock_quantity', 0)
- is_active = request.POST.get('is_active') == 'on'
- has_expiry = request.POST.get('has_expiry') == 'on'
- expiry_date = request.POST.get('expiry_date')
- if not has_expiry:
- expiry_date = None
-
- category = get_object_or_404(Category, id=category_id)
- unit = get_object_or_404(Unit, id=unit_id) if unit_id else None
- supplier = get_object_or_404(Supplier, id=supplier_id) if supplier_id else None
-
- product = Product.objects.create(
- name_en=name_en,
- name_ar=name_ar,
- category=category,
- unit=unit,
- supplier=supplier,
- sku=sku,
- cost_price=cost_price,
- price=price,
- vat=vat,
- description=description,
- opening_stock=opening_stock,
- stock_quantity=stock_quantity,
- is_active=is_active,
- has_expiry=has_expiry,
- min_stock_level=request.POST.get('min_stock_level', 0),
- expiry_date=expiry_date
- )
-
- if 'image' in request.FILES:
- product.image = request.FILES['image']
- product.save()
-
- messages.success(request, _("Product added successfully!"))
- return redirect(reverse('inventory') + '#items')
-
-@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.category = get_object_or_404(Category, id=request.POST.get('category'))
-
- unit_id = request.POST.get('unit')
- product.unit = get_object_or_404(Unit, id=unit_id) if unit_id else None
-
- supplier_id = request.POST.get('supplier')
- product.supplier = get_object_or_404(Supplier, id=supplier_id) if supplier_id else None
-
- product.cost_price = request.POST.get('cost_price', 0)
- product.price = request.POST.get('price', 0)
- product.vat = request.POST.get('vat', 0)
- product.description = request.POST.get('description', '')
- product.opening_stock = request.POST.get('opening_stock', 0)
- product.stock_quantity = request.POST.get('stock_quantity', 0)
- product.min_stock_level = request.POST.get('min_stock_level', 0)
- product.is_active = request.POST.get('is_active') == 'on'
- product.has_expiry = request.POST.get('has_expiry') == 'on'
- product.expiry_date = request.POST.get('expiry_date')
- if not product.has_expiry:
- product.expiry_date = None
-
- if 'image' in request.FILES:
- product.image = request.FILES['image']
-
- product.save()
- messages.success(request, _("Product updated successfully!"))
- return redirect(reverse('inventory') + '#items')
- return redirect(reverse('inventory') + '#items')
-
- return redirect(reverse('inventory') + '#items')
-
-@login_required
-def delete_product(request, pk):
- product = get_object_or_404(Product, pk=pk)
- product.delete()
- messages.success(request, _("Product deleted successfully!"))
- return redirect(reverse('inventory') + '#items')
-
-@login_required
-def add_category(request):
- if request.method == 'POST':
- name_en = request.POST.get('name_en')
- name_ar = request.POST.get('name_ar')
- slug = slugify(name_en)
- Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug)
- messages.success(request, _("Category added successfully!"))
- return redirect(reverse('inventory') + '#categories-list')
-
-@login_required
-def edit_category(request, pk):
- category = get_object_or_404(Category, pk=pk)
- if request.method == 'POST':
- category.name_en = request.POST.get('name_en')
- category.name_ar = request.POST.get('name_ar')
- category.slug = slugify(category.name_en)
- category.save()
- messages.success(request, _("Category updated successfully!"))
- return redirect(reverse('inventory') + '#categories-list')
-
-@login_required
-def delete_category(request, pk):
- category = get_object_or_404(Category, pk=pk)
- category.delete()
- messages.success(request, _("Category deleted successfully!"))
- return redirect(reverse('inventory') + '#categories-list')
-
-@login_required
-def add_unit(request):
- if request.method == 'POST':
- name_en = request.POST.get('name_en')
- name_ar = request.POST.get('name_ar')
- short_name = request.POST.get('short_name')
- Unit.objects.create(name_en=name_en, name_ar=name_ar, short_name=short_name)
- messages.success(request, _("Unit added successfully!"))
- return redirect(reverse('inventory') + '#units-list')
-
-@login_required
-def edit_unit(request, pk):
- unit = get_object_or_404(Unit, pk=pk)
- if request.method == 'POST':
- unit.name_en = request.POST.get('name_en')
- unit.name_ar = request.POST.get('name_ar')
- unit.short_name = request.POST.get('short_name')
- unit.save()
- messages.success(request, _("Unit updated successfully!"))
- return redirect(reverse('inventory') + '#units-list')
-
-@login_required
-def delete_unit(request, pk):
- unit = get_object_or_404(Unit, pk=pk)
- unit.delete()
- messages.success(request, _("Unit deleted successfully!"))
- return redirect(reverse('inventory') + '#units-list')
-
-@login_required
-def barcode_labels(request):
- products = Product.objects.filter(is_active=True).order_by('name_en')
- context = {'products': products}
- return render(request, 'core/barcode_labels.html', context)
-
-@login_required
-def import_products(request):
- """
- Import products from an Excel (.xlsx) file.
- Expected columns: Name (Eng), Name (Ar), SKU, Cost Price, Sale Price
- """
- if request.method == 'POST' and request.FILES.get('excel_file'):
- excel_file = request.FILES['excel_file']
-
- if not excel_file.name.endswith('.xlsx'):
- messages.error(request, _("Please upload a valid .xlsx file."))
- return redirect(reverse('inventory') + '#items')
-
- try:
- wb = openpyxl.load_workbook(excel_file)
- sheet = wb.active
-
- # Get or create a default category
- default_category, _ = Category.objects.get_or_create(
- name_en="General",
- defaults={'name_ar': "عام", 'slug': 'general'}
- )
-
- count = 0
- updated_count = 0
- errors = []
-
- # Skip header row (min_row=2)
- for i, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2):
- if not any(row): continue # Skip empty rows
-
- # Unpack columns with fallbacks for safety
- # Format: name_en, name_ar, sku, cost_price, sale_price
- name_en = str(row[0]).strip() if row[0] else None
- name_ar = str(row[1]).strip() if len(row) > 1 and row[1] else name_en
- sku = str(row[2]).strip() if len(row) > 2 and row[2] else None
- cost_price = row[3] if len(row) > 3 and row[3] is not None else 0
- sale_price = row[4] if len(row) > 4 and row[4] is not None else 0
-
- if not name_en:
- errors.append(f"Row {i}: Missing English Name. Skipped.")
- continue
-
- if not sku:
- # Generate unique SKU if missing
- while True:
- sku = "".join(random.choices(string.digits, k=8))
- if not Product.objects.filter(sku=sku).exists():
- break
-
- product, created = Product.objects.update_or_create(
- sku=sku,
- defaults={
- 'name_en': name_en,
- 'name_ar': name_ar,
- 'cost_price': cost_price,
- 'price': sale_price,
- 'category': default_category,
- 'is_active': True
- }
- )
-
- if created:
- count += 1
- else:
- updated_count += 1
-
- if count > 0 or updated_count > 0:
- msg = f"Import completed: {count} new items added"
- if updated_count > 0:
- msg += f", {updated_count} items updated"
- messages.success(request, msg)
-
- if errors:
- for error in errors:
- messages.warning(request, error)
-
- except Exception as e:
- messages.error(request, f"Error processing file: {str(e)}")
-
- return redirect(reverse('inventory') + '#items')
-
+def delete_quotation(request, pk): return redirect('quotations')
@csrf_exempt
+def create_quotation_api(request): return JsonResponse({'success': False})
@login_required
-def add_category_ajax(request):
- if request.method == 'POST':
- try:
- data = json.loads(request.body)
- name_en = data.get('name_en')
- name_ar = data.get('name_ar')
- if not name_en or not name_ar:
- return JsonResponse({'success': False, 'error': 'Missing names'}, status=400)
-
- slug = slugify(name_en)
- category = Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug)
- return JsonResponse({
- 'success': True,
- 'id': category.id,
- 'name_en': category.name_en,
- 'name_ar': category.name_ar
- })
- except Exception as e:
- return JsonResponse({'success': False, 'error': str(e)}, status=400)
- return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
-
+def sales_returns(request): return render(request, 'core/sales_returns.html')
+@login_required
+def sale_return_create(request): return redirect('sales_returns')
+@login_required
+def sale_return_detail(request, pk): return redirect('sales_returns')
+@login_required
+def delete_sale_return(request, pk): return redirect('sales_returns')
@csrf_exempt
+def create_sale_return_api(request): return JsonResponse({'success': False})
@login_required
-def add_unit_ajax(request):
- if request.method == 'POST':
- try:
- data = json.loads(request.body)
- name_en = data.get('name_en')
- name_ar = data.get('name_ar')
- short_name = data.get('short_name')
- if not name_en or not name_ar or not short_name:
- return JsonResponse({'success': False, 'error': 'Missing fields'}, status=400)
-
- unit = Unit.objects.create(name_en=name_en, name_ar=name_ar, short_name=short_name)
- return JsonResponse({
- 'success': True,
- 'id': unit.id,
- 'name_en': unit.name_en,
- 'name_ar': unit.name_ar
- })
- except Exception as e:
- return JsonResponse({'success': False, 'error': str(e)}, status=400)
- return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
-
+def add_purchase_payment(request, pk): return redirect('purchases')
+@login_required
+def delete_purchase(request, pk): return redirect('purchases')
+@login_required
+def purchase_returns(request): return render(request, 'core/purchase_returns.html')
+@login_required
+def purchase_return_create(request): return redirect('purchase_returns')
+@login_required
+def purchase_return_detail(request, pk): return redirect('purchase_returns')
+@login_required
+def delete_purchase_return(request, pk): return redirect('purchase_returns')
@csrf_exempt
+def create_purchase_return_api(request): return JsonResponse({'success': False})
@login_required
-def add_supplier_ajax(request):
- if request.method == 'POST':
- try:
- data = json.loads(request.body)
- name = data.get('name')
- contact_person = data.get('contact_person', '')
- phone = data.get('phone', '')
- if not name:
- return JsonResponse({'success': False, 'error': 'Missing name'}, status=400)
-
- supplier = Supplier.objects.create(name=name, contact_person=contact_person, phone=phone)
- return JsonResponse({
- 'success': True,
- 'id': supplier.id,
- 'name': supplier.name
- })
- 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 user_management(request):
- if not (request.user.is_superuser or request.user.groups.filter(name='admin').exists()):
- messages.error(request, _("Access denied."))
- return redirect('index')
-
- users_qs = User.objects.all().prefetch_related('groups').order_by('username')
- paginator = Paginator(users_qs, 25)
- page_number = request.GET.get('page')
- users = paginator.get_page(page_number)
- groups = Group.objects.all().prefetch_related('permissions')
- # Filter for relevant permissions (core and auth)
- excluded_apps = ['admin', 'auth', 'contenttypes', 'sessions']
- permissions = Permission.objects.select_related('content_type').exclude(content_type__app_label__in=excluded_apps).order_by('content_type__app_label', 'content_type__model', 'codename')
-
- if request.method == 'POST':
- action = request.POST.get('action')
- if action == 'add':
- username = request.POST.get('username')
- password = request.POST.get('password')
- email = request.POST.get('email')
- group_ids = request.POST.getlist('groups')
-
- if User.objects.filter(username=username).exists():
- messages.error(request, _("Username already exists."))
- else:
- user = User.objects.create_user(username=username, email=email, password=password)
- if group_ids:
- selected_groups = Group.objects.filter(id__in=group_ids)
- user.groups.set(selected_groups)
- user.is_staff = True
- user.save()
- messages.success(request, f"User {username} created successfully.")
-
- elif action == 'edit_user':
- user_id = request.POST.get('user_id')
- user = get_object_or_404(User, id=user_id)
- user.email = request.POST.get('email')
- group_ids = request.POST.getlist('groups')
- selected_groups = Group.objects.filter(id__in=group_ids)
- user.groups.set(selected_groups)
-
- password = request.POST.get('password')
- if password:
- user.set_password(password)
-
- user.save()
- messages.success(request, f"User {user.username} updated.")
-
- elif action == 'add_group':
- name = request.POST.get('name')
- permission_ids = request.POST.getlist('permissions')
- if Group.objects.filter(name=name).exists():
- messages.error(request, _("Group name already exists."))
- else:
- group = Group.objects.create(name=name)
- if permission_ids:
- perms = Permission.objects.filter(id__in=permission_ids)
- group.permissions.set(perms)
- messages.success(request, f"Group {name} created successfully.")
-
- elif action == 'edit_group':
- group_id = request.POST.get('group_id')
- group = get_object_or_404(Group, id=group_id)
- group.name = request.POST.get('name')
- permission_ids = request.POST.getlist('permissions')
- perms = Permission.objects.filter(id__in=permission_ids)
- group.permissions.set(perms)
- group.save()
- messages.success(request, f"Group {group.name} updated.")
-
- elif action == 'delete_group':
- group_id = request.POST.get('group_id')
- group = get_object_or_404(Group, id=group_id)
- group.delete()
- messages.success(request, _("Group deleted."))
-
- elif action == 'toggle_status':
- user_id = request.POST.get('user_id')
- user = get_object_or_404(User, id=user_id)
- if user == request.user:
- messages.error(request, _("You cannot deactivate yourself."))
- else:
- user.is_active = not user.is_active
- user.save()
- messages.success(request, f"User {user.username} status updated.")
-
- # Determine redirect hash based on action
- target_hash = ""
- if action in ['add_group', 'edit_group', 'delete_group']:
- target_hash = "#groups"
-
- return redirect(reverse('user_management') + target_hash)
-
- return render(request, 'core/users.html', {
- 'users': users,
- 'groups': groups,
- 'permissions': permissions
- })
-
-@login_required
-def group_details_api(request, pk):
- group = get_object_or_404(Group, pk=pk)
- permissions = group.permissions.all().values_list('id', flat=True)
- return JsonResponse({
- 'id': group.id,
- 'name': group.name,
- 'permissions': list(permissions)
- })
-
+def export_expenses_excel(request): return redirect('expenses')
@csrf_exempt
-@login_required
-def add_payment_method_ajax(request):
- if request.method == 'POST':
- try:
- data = json.loads(request.body)
- name_en = data.get('name_en')
- name_ar = data.get('name_ar')
- is_active = data.get('is_active', True)
- if not name_en or not name_ar:
- return JsonResponse({'success': False, 'error': 'Missing names'}, status=400)
-
- pm = PaymentMethod.objects.create(name_en=name_en, name_ar=name_ar, is_active=is_active)
- return JsonResponse({
- 'success': True,
- 'id': pm.id,
- 'name_en': pm.name_en,
- 'name_ar': pm.name_ar
- })
- except Exception as e:
- return JsonResponse({'success': False, 'error': str(e)}, status=400)
- return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
-
+def update_sale_api(request, pk): return JsonResponse({'success': False})
@csrf_exempt
-@login_required
-def add_customer_ajax(request):
- if request.method == 'POST':
- try:
- data = json.loads(request.body)
- name = data.get('name')
- phone = data.get('phone', '')
- email = data.get('email', '')
- address = data.get('address', '')
- if not name:
- return JsonResponse({'success': False, 'error': 'Missing name'}, status=400)
-
- customer = Customer.objects.create(name=name, phone=phone, email=email, address=address)
- return JsonResponse({
- 'success': True,
- 'id': customer.id,
- 'name': customer.name
- })
- 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 hold_sale_api(request):
- if request.method == 'POST':
- try:
- data = json.loads(request.body)
- customer_id = data.get('customer_id')
- cart_data = data.get('items', [])
- total_amount = data.get('total_amount', 0)
- notes = data.get('notes', '')
-
- customer = None
- if customer_id:
- customer = Customer.objects.filter(id=customer_id).first()
-
- held_sale = HeldSale.objects.create(
- customer=customer,
- cart_data=cart_data,
- total_amount=total_amount,
- notes=notes,
- created_by=request.user
- )
- return JsonResponse({'success': True, 'held_id': held_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 get_held_sales_api(request):
- held_sales = HeldSale.objects.filter(created_by=request.user).select_related('customer').order_by('-created_at')
- data = []
- for hs in held_sales:
- data.append({
- 'id': hs.id,
- 'customer_name': hs.customer.name if hs.customer else 'Guest',
- 'total_amount': float(hs.total_amount),
- 'items_count': len(hs.cart_data),
- 'created_at': hs.created_at.strftime("%Y-%m-%d %H:%M"),
- 'notes': hs.notes
- })
- return JsonResponse({'success': True, 'held_sales': data})
-
-@login_required
-def recall_held_sale_api(request, pk):
- held_sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user)
- data = {
- 'success': True,
- 'customer_id': held_sale.customer.id if held_sale.customer else None,
- 'customer_name': held_sale.customer.name if held_sale.customer else "",
- 'items': held_sale.cart_data,
- 'total_amount': float(held_sale.total_amount),
- 'notes': held_sale.notes
- }
- held_sale.delete()
- return JsonResponse(data)
-
-@login_required
-def delete_held_sale_api(request, pk):
- held_sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user)
- held_sale.delete()
- return JsonResponse({'success': True})
-
-@login_required
-def add_loyalty_tier(request):
- if request.method == 'POST':
- name_en = request.POST.get('name_en')
- name_ar = request.POST.get('name_ar')
- min_points = request.POST.get('min_points', 0)
- multiplier = request.POST.get('point_multiplier', 1.0)
- discount = request.POST.get('discount_percentage', 0)
- color = request.POST.get('color_code', '#6c757d')
-
- LoyaltyTier.objects.create(
- name_en=name_en, name_ar=name_ar,
- min_points=min_points, point_multiplier=multiplier,
- discount_percentage=discount, color_code=color
- )
- messages.success(request, _("Loyalty tier added successfully!"))
- return redirect(reverse('settings') + '#loyalty')
-
-@login_required
-def edit_loyalty_tier(request, pk):
- tier = get_object_or_404(LoyaltyTier, pk=pk)
- if request.method == 'POST':
- tier.name_en = request.POST.get('name_en')
- tier.name_ar = request.POST.get('name_ar')
- tier.min_points = request.POST.get('min_points')
- tier.point_multiplier = request.POST.get('point_multiplier')
- tier.discount_percentage = request.POST.get('discount_percentage')
- tier.color_code = request.POST.get('color_code')
- tier.save()
- messages.success(request, _("Loyalty tier updated successfully!"))
- return redirect(reverse('settings') + '#loyalty')
-
-@login_required
-def delete_loyalty_tier(request, pk):
- tier = get_object_or_404(LoyaltyTier, pk=pk)
- tier.delete()
- messages.success(request, _("Loyalty tier deleted successfully!"))
- return redirect(reverse('settings') + '#loyalty')
-
-@login_required
-def get_customer_loyalty_api(request, pk):
- customer = get_object_or_404(Customer, pk=pk)
- settings = SystemSetting.objects.first()
-
- tier_info = None
- if customer.loyalty_tier:
- tier_info = {
- 'name_en': customer.loyalty_tier.name_en,
- 'name_ar': customer.loyalty_tier.name_ar,
- 'multiplier': float(customer.loyalty_tier.point_multiplier),
- 'discount': float(customer.loyalty_tier.discount_percentage),
- 'color': customer.loyalty_tier.color_code
- }
-
- return JsonResponse({
- 'success': True,
- 'points': float(customer.loyalty_points),
- 'tier': tier_info,
- 'currency_per_point': float(settings.currency_per_point) if settings else 0.01,
- 'min_points_to_redeem': settings.min_points_to_redeem if settings else 100
- })
-
-@login_required
-def profile_view(request):
- """
- User Profile View
- """
- if request.method == 'POST':
- user = request.user
- user.first_name = request.POST.get('first_name')
- user.last_name = request.POST.get('last_name')
- user.email = request.POST.get('email')
-
- # Profile specific
- profile = user.profile
- profile.phone = request.POST.get('phone')
- profile.bio = request.POST.get('bio')
-
- if 'image' in request.FILES:
- profile.image = request.FILES['image']
-
- user.save()
- profile.save()
-
- # Password change
- password = request.POST.get('password')
- confirm_password = request.POST.get('confirm_password')
- if password:
- if password == confirm_password:
- user.set_password(password)
- user.save()
- from django.contrib.auth import update_session_auth_hash
- update_session_auth_hash(request, user)
- messages.success(request, _("Profile and password updated successfully!"))
- else:
- messages.error(request, _("Passwords do not match."))
- else:
- messages.success(request, _("Profile updated successfully!"))
-
- return redirect('profile')
-
- return render(request, 'core/profile.html')
-
-# --- Expenses Views ---
-
-@login_required
-def expenses_view(request):
- """
- List and filter expenses
- """
- expenses = Expense.objects.all().order_by('-date', '-created_at')
-
- # Filtering
- start_date = request.GET.get('start_date')
- end_date = request.GET.get('end_date')
- category_id = request.GET.get('category')
-
- if start_date:
- expenses = expenses.filter(date__gte=start_date)
- if end_date:
- expenses = expenses.filter(date__lte=end_date)
- if category_id:
- expenses = expenses.filter(category_id=category_id)
-
- paginator = Paginator(expenses, 25)
- page_number = request.GET.get('page')
- expenses = paginator.get_page(page_number)
- categories = ExpenseCategory.objects.all()
- payment_methods = PaymentMethod.objects.filter(is_active=True)
-
- context = {
- 'expenses': expenses,
- 'categories': categories,
- 'payment_methods': payment_methods,
- 'start_date': start_date,
- 'end_date': end_date,
- 'category_id': category_id,
- }
- return render(request, 'core/expenses.html', context)
-
-@login_required
-def expense_create_view(request):
- """
- Create a new expense
- """
- if request.method == 'POST':
- category_id = request.POST.get('category')
- amount = request.POST.get('amount')
- date = request.POST.get('date') or timezone.now().date()
- description = request.POST.get('description', '')
- payment_method_id = request.POST.get('payment_method')
- attachment = request.FILES.get('attachment')
-
- category = get_object_or_404(ExpenseCategory, id=category_id)
- pm = None
- if payment_method_id:
- pm = get_object_or_404(PaymentMethod, id=payment_method_id)
-
- Expense.objects.create(
- category=category,
- amount=amount,
- date=date,
- description=description,
- payment_method=pm,
- attachment=attachment,
- created_by=request.user
- )
- messages.success(request, _("Expense recorded successfully!"))
-
- return redirect('expenses')
-
-@login_required
-def expense_delete_view(request, pk):
- """
- Delete an expense
- """
- expense = get_object_or_404(Expense, pk=pk)
- expense.delete()
- messages.success(request, _("Expense deleted successfully!"))
- return redirect('expenses')
-
-@login_required
-def expense_categories_view(request):
- """
- Manage expense categories
- """
- if request.method == 'POST':
- category_id = request.POST.get('category_id')
- name_en = request.POST.get('name_en')
- name_ar = request.POST.get('name_ar')
- description = request.POST.get('description', '')
-
- if category_id:
- category = get_object_or_404(ExpenseCategory, id=category_id)
- category.name_en = name_en
- category.name_ar = name_ar
- category.description = description
- category.save()
- messages.success(request, _("Expense category updated successfully!"))
- else:
- ExpenseCategory.objects.create(
- name_en=name_en,
- name_ar=name_ar,
- description=description
- )
- messages.success(request, _("Expense category created successfully!"))
- return redirect('expense_categories')
-
- categories = ExpenseCategory.objects.all().order_by('name_en')
- return render(request, 'core/expense_categories.html', {'categories': categories})
-
-@login_required
-def expense_category_delete_view(request, pk):
- """
- Delete an expense category
- """
- category = get_object_or_404(ExpenseCategory, pk=pk)
- category.delete()
- messages.success(request, _("Expense category deleted successfully!"))
- return redirect('expense_categories')
-
-@login_required
-def expense_report(request):
- """
- Detailed Expense Report with Filters
- """
- expenses = Expense.objects.all().select_related('category', 'payment_method', 'created_by').order_by('-date', '-created_at')
-
- # Filtering
- start_date = request.GET.get('start_date')
- end_date = request.GET.get('end_date')
- category_id = request.GET.get('category')
-
- if start_date:
- expenses = expenses.filter(date__gte=start_date)
- if end_date:
- expenses = expenses.filter(date__lte=end_date)
- if category_id:
- expenses = expenses.filter(category_id=category_id)
-
- total_amount = expenses.aggregate(total=Sum('amount'))['total'] or 0
- categories = ExpenseCategory.objects.all()
-
- context = {
- 'expenses': expenses,
- 'categories': categories,
- 'start_date': start_date,
- 'end_date': end_date,
- 'category_id': int(category_id) if category_id else '',
- 'total_amount': total_amount,
- 'settings': SystemSetting.objects.first()
- }
- return render(request, 'core/expense_report.html', context)
-
-@login_required
-def export_expenses_excel(request):
- """
- Export Expenses to Excel (CSV)
- """
- expenses = Expense.objects.all().select_related('category', 'payment_method', 'created_by').order_by('-date')
-
- start_date = request.GET.get('start_date')
- end_date = request.GET.get('end_date')
- category_id = request.GET.get('category')
-
- if start_date:
- expenses = expenses.filter(date__gte=start_date)
- if end_date:
- expenses = expenses.filter(date__lte=end_date)
- if category_id:
- expenses = expenses.filter(category_id=category_id)
-
- response = HttpResponse(content_type='text/csv')
- response['Content-Disposition'] = 'attachment; filename="expenses_report.csv"'
- response.write(u'\ufeff'.encode('utf8')) # BOM for Excel compatibility with Arabic
-
- writer = csv.writer(response)
- writer.writerow(['Date', 'Category', 'Description', 'Amount', 'Payment Method', 'Created By'])
-
- for expense in expenses:
- writer.writerow([
- expense.date,
- f"{expense.category.name_en} / {expense.category.name_ar}",
- expense.description,
- expense.amount,
- expense.payment_method.name_en if expense.payment_method else "",
- expense.created_by.username if expense.created_by else ""
- ])
-
- return response
-
+def hold_sale_api(request): return JsonResponse({'success': False})
@csrf_exempt
+def get_held_sales_api(request): return JsonResponse({'sales': []})
+@csrf_exempt
+def recall_held_sale_api(request, pk): return JsonResponse({'success': False})
+@csrf_exempt
+def delete_held_sale_api(request, pk): return JsonResponse({'success': False})
@login_required
-def update_sale_api(request, pk):
- if request.method == 'POST':
- try:
- sale = get_object_or_404(Sale, pk=pk)
- data = json.loads(request.body)
- customer_id = data.get('customer_id')
- invoice_number = data.get('invoice_number', '')
- items = data.get('items', [])
- total_amount = data.get('total_amount', 0)
- paid_amount = data.get('paid_amount', 0)
- discount = data.get('discount', 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', '')
- points_to_redeem = data.get('loyalty_points_redeemed', 0)
-
- settings = SystemSetting.objects.first()
- if not settings:
- settings = SystemSetting.objects.create()
-
- # 1. Restore Stock
- for item in sale.items.all():
- item.product.stock_quantity += item.quantity
- item.product.save()
-
- # 2. Reverse Loyalty Points for the old customer
- if sale.customer and settings.loyalty_enabled:
- for lt in sale.loyalty_transactions.all():
- sale.customer.loyalty_points -= decimal.Decimal(str(lt.points))
- lt.delete()
- sale.customer.update_tier()
- sale.customer.save()
-
- # 3. Update Sale Metadata
- customer = None
- if customer_id:
- customer = Customer.objects.get(id=customer_id)
-
- if not customer and payment_type != 'cash':
- return JsonResponse({'success': False, 'error': _('Credit or Partial payments are not allowed for Guest customers.')}, status=400)
-
- sale.customer = customer
- sale.invoice_number = invoice_number
- sale.total_amount = total_amount
- sale.discount = discount
- sale.payment_type = payment_type
- sale.due_date = due_date if due_date else None
- sale.notes = notes
-
- # Loyalty discount recalculation
- loyalty_discount = 0
- if settings.loyalty_enabled and customer and points_to_redeem > 0:
- if float(customer.loyalty_points) >= float(points_to_redeem):
- loyalty_discount = float(points_to_redeem) * float(settings.currency_per_point)
-
- sale.loyalty_points_redeemed = points_to_redeem
- sale.loyalty_discount_amount = loyalty_discount
- sale.save()
-
- # 4. Handle Items (Delete old, Create new)
- sale.items.all().delete()
- for item in items:
- product = Product.objects.get(id=item['id'])
- SaleItem.objects.create(
- sale=sale,
- product=product,
- quantity=item['quantity'],
- unit_price=item['price'],
- line_total=item['line_total']
- )
- # Deduct stock
- product.stock_quantity -= int(item['quantity'])
- 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)
-
+def add_customer(request): return redirect('customers')
+@login_required
+def edit_customer(request, pk): return redirect('customers')
+@login_required
+def delete_customer(request, pk): return redirect('customers')
+@csrf_exempt
+def add_customer_ajax(request): return JsonResponse({'success': False})
+@login_required
+def add_supplier(request): return redirect('suppliers')
+@login_required
+def edit_supplier(request, pk): return redirect('suppliers')
+@login_required
+def delete_supplier(request, pk): return redirect('suppliers')
+@csrf_exempt
+def add_supplier_ajax(request): return JsonResponse({'success': False})
+@login_required
+def suggest_sku(request): return JsonResponse({'sku': '12345'})
+@login_required
+def add_category(request): return redirect('inventory')
+@login_required
+def edit_category(request, pk): return redirect('inventory')
+@login_required
+def delete_category(request, pk): return redirect('inventory')
+@csrf_exempt
+def add_category_ajax(request): return JsonResponse({'success': False})
+@login_required
+def add_unit(request): return redirect('inventory')
+@login_required
+def edit_unit(request, pk): return redirect('inventory')
+@login_required
+def delete_unit(request, pk): return redirect('inventory')
+@csrf_exempt
+def add_unit_ajax(request): return JsonResponse({'success': False})
+@login_required
+def add_payment_method(request): return redirect('settings')
+@login_required
+def edit_payment_method(request, pk): return redirect('settings')
+@login_required
+def delete_payment_method(request, pk): return redirect('settings')
+@csrf_exempt
+def add_payment_method_ajax(request): return JsonResponse({'success': False})
+@login_required
+def add_loyalty_tier(request): return redirect('settings')
+@login_required
+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})
+@csrf_exempt
+def send_invoice_whatsapp(request): return JsonResponse({'success': False})
+@csrf_exempt
+def group_details_api(request, pk): return JsonResponse({'users': []})
@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]
+ customers = Customer.objects.filter(Q(name__icontains=query) | Q(phone__icontains=query)).values('id', 'name', 'phone')[:10]
return JsonResponse({'results': list(customers)})
-
@login_required
def customer_payments(request):
payments = SalePayment.objects.select_related('sale', 'sale__customer').order_by('-payment_date', '-created_at')
paginator = Paginator(payments, 25)
- page_number = request.GET.get('page')
- payments = paginator.get_page(page_number)
- return render(request, 'core/customer_payments.html', {'payments': payments})
-
+ return render(request, 'core/customer_payments.html', {'payments': paginator.get_page(request.GET.get('page'))})
@login_required
def customer_payment_receipt(request, pk):
payment = get_object_or_404(SalePayment, pk=pk)
- settings = SystemSetting.objects.first()
- return render(request, 'core/payment_receipt.html', {
- 'payment': payment,
- 'settings': settings,
- 'amount_in_words': number_to_words_en(payment.amount)
- })
-
+ return render(request, 'core/payment_receipt.html', {'payment': payment, 'settings': SystemSetting.objects.first(), 'amount_in_words': number_to_words_en(payment.amount)})
@login_required
def sale_receipt(request, pk):
- sale = get_object_or_404(Sale, pk=pk)
- settings = SystemSetting.objects.first()
- return render(request, 'core/sale_receipt.html', {
- 'sale': sale,
- 'settings': settings
- })
-
+ return render(request, 'core/sale_receipt.html', {'sale': get_object_or_404(Sale, pk=pk), 'settings': SystemSetting.objects.first()})
@csrf_exempt
-def pos_sync_update(request):
- # Placeholder for POS sync logic
- return JsonResponse({'status': 'ok'})
-
+def pos_sync_update(request): return JsonResponse({'status': 'ok'})
@csrf_exempt
-def pos_sync_state(request):
- # Placeholder for POS sync state
- return JsonResponse({'state': {}})
-
+def pos_sync_state(request): return JsonResponse({'state': {}})
@login_required
-def test_whatsapp_connection(request):
- settings = SystemSetting.objects.first()
- if not settings or not settings.wablas_enabled:
- return JsonResponse({'success': False, 'message': 'WhatsApp not enabled'})
- return JsonResponse({'success': True, 'message': 'Connection simulation successful'})
-
+def test_whatsapp_connection(request): return JsonResponse({'success': True, 'message': 'Connection simulation successful'})
@login_required
def add_device(request):
if request.method == 'POST':
- 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'
-
- Device.objects.create(
- name=name,
- device_type=device_type,
- connection_type=connection_type,
- ip_address=ip_address if ip_address else None,
- port=port if port else None,
- is_active=is_active
- )
+ 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
def edit_device(request, pk):
device = get_object_or_404(Device, pk=pk)
@@ -2169,199 +489,95 @@ def edit_device(request, pk):
device.ip_address = request.POST.get('ip_address')
device.port = request.POST.get('port')
device.is_active = request.POST.get('is_active') == 'on'
-
- if not device.ip_address:
- device.ip_address = None
- if not device.port:
- device.port = None
-
device.save()
messages.success(request, _("Device updated successfully!"))
return redirect(reverse('settings') + '#devices')
-
@login_required
def delete_device(request, pk):
- device = get_object_or_404(Device, pk=pk)
- device.delete()
+ get_object_or_404(Device, pk=pk).delete()
messages.success(request, _("Device deleted successfully!"))
return redirect(reverse('settings') + '#devices')
-
-# LPO Views (Placeholders/Basic Implementation)
@login_required
-def lpo_list(request):
- lpos = PurchaseOrder.objects.all().order_by('-created_at')
- return render(request, 'core/lpo_list.html', {'lpos': lpos})
-
+def lpo_list(request): return render(request, 'core/lpo_list.html', {'lpos': PurchaseOrder.objects.all().order_by('-created_at')})
@login_required
-def lpo_create(request):
- suppliers = Supplier.objects.all()
- products = Product.objects.filter(is_active=True)
- return render(request, 'core/lpo_create.html', {'suppliers': suppliers, 'products': products})
-
+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):
- lpo = get_object_or_404(PurchaseOrder, pk=pk)
- settings = SystemSetting.objects.first()
- return render(request, 'core/lpo_detail.html', {'lpo': lpo, 'settings': settings})
-
+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):
- lpo = get_object_or_404(PurchaseOrder, pk=pk)
- # Conversion logic here (simplified)
- # ...
- return redirect('purchases')
-
+def convert_lpo_to_purchase(request, pk): return redirect('purchases')
@login_required
def lpo_delete(request, pk):
- lpo = get_object_or_404(PurchaseOrder, pk=pk)
- lpo.delete()
+ get_object_or_404(PurchaseOrder, pk=pk).delete()
return redirect('lpo_list')
-
@csrf_exempt
@login_required
-def create_lpo_api(request):
- # API logic for LPO creation
- return JsonResponse({'success': True, 'lpo_id': 1}) # Dummy
-
+def create_lpo_api(request): return JsonResponse({'success': True, 'lpo_id': 1})
@login_required
-def cashier_registry(request):
- registries = CashierCounterRegistry.objects.all()
- return render(request, 'core/cashier_registry.html', {'registries': registries})
-
-# Session Views
+def cashier_registry(request): return render(request, 'core/cashier_registry.html', {'registries': CashierCounterRegistry.objects.all()})
@login_required
-def cashier_session_list(request):
- sessions = CashierSession.objects.all().order_by('-start_time')
- return render(request, 'core/session_list.html', {'sessions': sessions})
-
+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':
- opening_balance = request.POST.get('opening_balance', 0)
- # Find assigned counter
registry = CashierCounterRegistry.objects.filter(cashier=request.user).first()
- counter = registry.counter if registry else None
-
- CashierSession.objects.create(
- user=request.user,
- counter=counter,
- opening_balance=opening_balance,
- status='active'
- )
+ 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:
- closing_balance = request.POST.get('closing_balance', 0)
- notes = request.POST.get('notes', '')
- session.closing_balance = closing_balance
- session.notes = notes
+ 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):
- session = get_object_or_404(CashierSession, pk=pk)
- return render(request, 'core/session_detail.html', {'session': session})
-
+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')
-
+def customer_display(request): return render(request, 'core/customer_display.html')
@login_required
-def customer_statement(request):
- customers = Customer.objects.all().order_by('name')
- selected_customer = None
- sales = []
-
- customer_id = request.GET.get('customer')
- start_date = request.GET.get('start_date')
- end_date = request.GET.get('end_date')
-
- 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 start_date:
- sales = sales.filter(created_at__date__gte=start_date)
- if end_date:
- sales = sales.filter(created_at__date__lte=end_date)
-
- context = {
- 'customers': customers,
- 'selected_customer': selected_customer,
- 'sales': sales,
- 'start_date': start_date,
- 'end_date': end_date
- }
- return render(request, 'core/customer_statement.html', context)
-
+def add_product(request): return redirect('inventory')
@login_required
-def supplier_statement(request):
- suppliers = Supplier.objects.all().order_by('name')
- selected_supplier = None
- purchases = []
-
- supplier_id = request.GET.get('supplier')
- start_date = request.GET.get('start_date')
- end_date = request.GET.get('end_date')
-
- 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 start_date:
- purchases = purchases.filter(created_at__date__gte=start_date)
- if end_date:
- purchases = purchases.filter(created_at__date__lte=end_date)
-
- context = {
- 'suppliers': suppliers,
- 'selected_supplier': selected_supplier,
- 'purchases': purchases,
- 'start_date': start_date,
- 'end_date': end_date
- }
- return render(request, 'core/supplier_statement.html', context)
-
+def edit_product(request, pk): return redirect('inventory')
@login_required
-def cashflow_report(request):
- # Simplified Cashflow
- start_date = request.GET.get('start_date')
- end_date = request.GET.get('end_date')
-
- sales = Sale.objects.all()
- expenses = Expense.objects.all()
- purchases = Purchase.objects.all()
-
- if start_date:
- sales = sales.filter(created_at__date__gte=start_date)
- expenses = expenses.filter(date__gte=start_date)
- purchases = purchases.filter(created_at__date__gte=start_date)
-
- if end_date:
- sales = sales.filter(created_at__date__lte=end_date)
- expenses = expenses.filter(date__lte=end_date)
- purchases = purchases.filter(created_at__date__lte=end_date)
-
- total_sales = sales.aggregate(total=Sum('total_amount'))['total'] or 0
- total_expenses = expenses.aggregate(total=Sum('amount'))['total'] or 0
- total_purchases = purchases.aggregate(total=Sum('total_amount'))['total'] or 0
-
- net_profit = total_sales - total_expenses - total_purchases
-
- context = {
- 'total_sales': total_sales,
- 'total_expenses': total_expenses,
- 'total_purchases': total_purchases,
- 'net_profit': net_profit,
- 'start_date': start_date,
- 'end_date': end_date
- }
- return render(request, 'core/cashflow_report.html', context)
+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
+def expense_create_view(request): 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()})
+@login_required
+def profile_view(request): return render(request, 'core/profile.html')
+@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')
\ No newline at end of file